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.
Content-Typeandapi-targetin your request, the server's CORS configuration must explicitly allow those request headers.expressAppinside ofcreateServerHttp()Instead you could callexpressApp.listen(3000, () => {console.log('Server is up')})