0

Why do I get a CORS error when attempting to fetch data from my loopback server hosted on AWS, despite having set the origin to "*"?

I am trying to fetch data from my remotely hosted loopback server to a simple html-page using the following code:

const headers = {
  'Content-Type': 'application/json',
  'api-target': 'rest'
};
fetch("https://nightli-staging-server.link/public-guest-lists/1", {
  headers: headers
})
  .then(response => response.json())
  .then(data => {
    const widget = document.getElementById("widget");
    widget.innerHTML = JSON.stringify(data);
  });

However, I receive the following CORS error:

Access to fetch at 'https://nightli-staging-server.link/public-guest-lists/1' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

I have set the origin to "*" in my index.ts file, which is used to start the loopback server. Here is the relevant part of the index.ts file:

const corsOptions = {
  origin: '*',
  credentials: true,
  exposedHeaders: ['Access-Control-Allow-Origin', 'Access-Control-Allow-Headers'],
};
expressApp.use(cors(corsOptions));

Why am I still encountering a CORS error? What else could be causing this issue, and how can I resolve it?

Posting relevant files from the remotely hosted loopback server (should it help):

index.ts:

import aws from 'aws-sdk';
import cors from 'cors';
import express, {NextFunction} from 'express';
import {graphqlHTTP} from 'express-graphql';
import {createServer as createServerHttp} from 'http';
import {createGraphQLSchema, Oas3} from 'openapi-to-graphql';
import qs from 'qs';
import {ApplicationConfig, NightliBackendApplication} from './application';

export * from './application';

export async function main(options: ApplicationConfig = {}) {
  const expressApp = express();
  let url = `${process.env.HOST}:${process.env.PORT ?? 3000}`;

  const app = new NightliBackendApplication(options);
  await app.boot();
  await app.start();
  url = app.restServer.url!;
  console.log(`Server is running at ${url}`);
  console.log(`Connected to database ${process.env.DB_DATABASE}`);
  const oas = await app.restServer.getApiSpec();
  const graphResult = await createGraphQLSchema(oas as Oas3, {
    strict: false,
    viewer: false,
    singularNames: true,
    fillEmptyResponses: true,
    baseUrl: url,
    headers: {
      'X-Origin': 'GraphQL',
    },
    tokenJSONpath: '$.jwt',
  });
  const {schema} = graphResult;
  const corsOptions = {
    origin: '*',
    credentials: true,
    exposedHeaders: ['Access-Control-Allow-Origin', 'Access-Control-Allow-Headers'],
    headers: ['Content-Type', 'api-target']
  };
  expressApp.use(cors(corsOptions));

  expressApp.use(express.json({limit: '50mb'}));
  expressApp.use(express.urlencoded({limit: '50mb'}));
  // For AWS ALB health checks
  expressApp.get('/', (req, res) => {
    res.sendStatus(200);
  });
  expressApp.use(
    '/graphql',
    // eslint-disable-next-line @typescript-eslint/no-misused-promises
    graphqlHTTP(request => ({
      schema,
      graphiql: true,
      context: {
        jwt: request.headers.authorization?.replace(/^Bearer /, ''),
      },
    })),
  );

  aws.config.update({
    accessKeyId: process.env.AWS_ACCESS_KEY,
    secretAccessKey: process.env.AWS_SECRET_KEY,
    region: process.env.AWS_REGION,
  });

  const server = createServerHttp(expressApp);
  const graphqlPort = process.env.GRAPHQL_PORT ?? 3001;
  server.listen(graphqlPort, () => {
    console.log(
      `GraphQL is running at http://${process.env.HOST}:${graphqlPort}/graphql`,
    );
  });

  return app;
}

if (require.main === module) {
  // Run the application
  const config: ApplicationConfig = {
    rest: {
      port: +(process.env.PORT ?? 3000),
      host: process.env.HOST,
      // The `gracePeriodForClose` provides a graceful close for http/https
      // servers with keep-alive clients. The default value is `Infinity`
      // (don't force-close). If you want to immediately destroy all sockets
      // upon stop, set its value to `0`.
      // See https://www.npmjs.com/package/stoppable
      gracePeriodForClose: 5000, // 5 seconds
      cors: {
        origin: '*',
        methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
        preflightContinue: true,
        credentials: true,
        allowedHeaders: 'Content-Type, api-target',
        exposedHeaders: ['Access-Control-Allow-Origin', 'Access-Control-Allow-Headers'],
            // Add a function to log when an OPTIONS request is received
        optionsSuccess: function(req:Request, res:Response, next:NextFunction) {
          const origin = req.headers.get('origin');
          console.log("in optionsSucces!")
          console.log("req", req)
          if (origin) {
            console.log('Received OPTIONS request from:', origin);
          } else {
            console.log('Received OPTIONS request without Origin header.');
          }
          next();
        }
      },
      openApiSpec: {
        // useful when used with OpenAPI-to-GraphQL to locate your application
        setServersFromRequest: true,
      },
      expressSettings: {
        'query parser': function (value: string, option: any) {
          return qs.parse(value, {
            arrayLimit: 800,
          });
        },
      },
    },
  };
  console.log("CORS config:", config.rest.cors);
  main(config).catch(err => {
    console.error('Cannot start the application.', err);
    process.exit(1);
  });
}

controller with endpoint being called on:

import {inject} from '@loopback/core';
import {
  Request,
  RestBindings,
  SchemaObject,
  get,
  param,
  post,
  requestBody,
  response
} from '@loopback/rest';
import {ServerResponse} from 'http';
import {EventItemServiceBindings, GuestListServiceBindings} from '../keys';
import {CreatePublicGuestlist, GetPublicGuestListSchema, messageSchemaResponse} from '../schemas';
import {IEventItemService, IGuestListService} from '../services';
import {PublicGuestListPayload, PublicGuestListType} from '../types';


export class PublicGuestListController {
  constructor(
    @inject(GuestListServiceBindings.GUEST_LIST_SERVICE)
    public guestListService: IGuestListService,
    @inject(EventItemServiceBindings.EVENT_ITEM_SERVICE)
    public eventItemService: IEventItemService,
  ) { }

  @post('/public-guest-lists/{clubId}')
  @response(200, {
    description: 'Create public guest and add to public guest list',
    content: {'application/json': {schema: messageSchemaResponse}},
  })
  async createPublicGuestList(
    @param.path.number('clubId') clubId: number,
    @requestBody({
      content: {
        'application/json': {
          schema: CreatePublicGuestlist as SchemaObject,
        },
      },
    })
    payload: PublicGuestListPayload,
    @inject(RestBindings.Http.REQUEST)
    request: Request,
    @inject(RestBindings.Http.RESPONSE) response: ServerResponse,
  ): Promise<{message: string}> {
    await this.guestListService.addToPublicGuestList(clubId, payload);

    // Set Access-Control-Allow-Origin header to allow all connections
    response.setHeader('Access-Control-Allow-Origin', '*');

    return {message: 'Request sent'};
  }

  @get('/public-guest-lists/{clubId}')
  @response(200, {
    description: 'Get public guest lists (event_items) for club',
    content: {'application/json': {schema: GetPublicGuestListSchema}},
  })
  async getPublicGuestLists(
    @param.path.number('clubId') clubId: number,
    @inject(RestBindings.Http.RESPONSE) response: ServerResponse,
  ): Promise<PublicGuestListType[]> {
    const publicGuestLists = await this.eventItemService.getPublicEventItemsByClub(clubId);

    // Set Access-Control-Allow-Origin header to allow all connections
    response.setHeader('Access-Control-Allow-Origin', '*');

    return publicGuestLists;
  }


}

sequence.ts file:

import {MiddlewareSequence} from '@loopback/rest';

export class MySequence extends MiddlewareSequence {}

application.ts file:

import {AuthenticationComponent} from '@loopback/authentication';
import {
  AuthorizationBindings,
  AuthorizationComponent,
  AuthorizationDecision,
} from '@loopback/authorization';
import {BootMixin} from '@loopback/boot';
import {ApplicationConfig} from '@loopback/core';
import {CronComponent} from '@loopback/cron';
import {RepositoryMixin} from '@loopback/repository';
import {RestApplication} from '@loopback/rest';
import {
  RestExplorerBindings,
  RestExplorerComponent,
} from '@loopback/rest-explorer';
import {ServiceMixin} from '@loopback/service-proxy';
import path from 'path';
import {BindingComponent} from './bindings';
import {PostgresqlDataSource} from './datasources';
import {UserServiceBindings} from './keys';
import {MySequence} from './sequence';

export {ApplicationConfig};

export class NightliBackendApplication extends BootMixin(
  ServiceMixin(RepositoryMixin(RestApplication)),
) {
  constructor(options: ApplicationConfig = {}) {
    super(options);

    // Set up the custom sequence
    this.sequence(MySequence);

    // Set up default home page
    this.static('/', path.join(__dirname, '../public'));

    // Customize @loopback/rest-explorer configuration here
    this.configure(RestExplorerBindings.COMPONENT).to({
      path: '/explorer',
    });
    this.component(RestExplorerComponent);
    // Mount authentication system
    this.component(AuthenticationComponent);
    // Mount authorization system
    this.configure(AuthorizationBindings.COMPONENT).to({
      precedence: AuthorizationDecision.DENY,
      defaultDecision: AuthorizationDecision.DENY,
    });
    this.component(AuthorizationComponent);
    // Mount jwt component
    this.component(BindingComponent);

    //  Cron job component
    this.component(CronComponent);

    // Bind datasource
    this.dataSource(PostgresqlDataSource, UserServiceBindings.DATASOURCE_NAME);
    this.projectRoot = __dirname;

    // logger
    // const fileTransport: DailyRotateFile = new DailyRotateFile({
    //   level: 'error',
    //   filename: 'logs/application-%DATE%.log',
    //   datePattern: 'YYYY-MM-DD',
    //   format: combine(label({label: 'Error log!'}), timestamp(), errorFormat),
    //   zippedArchive: true,
    //   maxSize: '20m',
    //   maxFiles: '14d',
    // });

    // this.bind('logging.winston.transports.file')
    //   .to(fileTransport)
    //   .apply(extensionFor(WINSTON_TRANSPORT));

    // this.configure(LoggingBindings.COMPONENT).to({
    //   enableFluent: false,
    //   enableHttpAccessLog: true,
    // });

    // this.component(LoggingComponent);

    // Customize @loopback/boot Booter Conventions here
    this.bootOptions = {
      controllers: {
        // Customize ControllerBooter Conventions here
        dirs: ['controllers'],
        extensions: ['.controller.js'],
        nested: true,
      },
    };
  }
}

I confirmed that the endpoint is functioning correctly by adding a "api-target:rest" header to the fetch request in my browser. This overrode the preflight request headers and bypassed CORS restrictions, allowing the expected response body to be received successfully.

Update:

When hosting the client-side on a AWS S3 bucket I instead get this CORS error:

"Acess to fetch at 'https://nightli-staging-server.link/public-guest-lists/1' from origin 'https://nightli-widget.s3.eu-north-1.amazonaws.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status."

I have also now included console.logs in the index.ts file here

optionsSuccess: function(req:Request, res:Response, next:NextFunction) {
      const origin = req.headers.get('origin');
      console.log("in optionsSucces!")
      console.log("req", req)
      if (origin) {
        console.log('Received OPTIONS request from:', origin);
      } else {
        console.log('Received OPTIONS request without Origin header.');
      }
      next();
    }
  }

Without any logs showing in console.

5
  • Because you're including custom headers Content-Type and api-target in your request, the server's CORS configuration must explicitly allow those request headers. Commented Apr 21, 2023 at 11:45
  • Thanks for the answer. I added headers: ['Content-Type', 'api-target'] to the corsOptions in the index.ts file. Unfortunately, the same issue persists Commented Apr 21, 2023 at 12:09
  • What AWS service is being used? Commented Apr 21, 2023 at 12:35
  • Sending a docker image of the server to ECR and then hosting it through ECS Commented Apr 21, 2023 at 12:44
  • I'm confused to as why you passing expressApp inside of createServerHttp() Instead you could call expressApp.listen(3000, () => {console.log('Server is up')}) Commented Apr 21, 2023 at 13:14

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.