I'm a newbie in Cloudflare Workers.
How to set CORS in Cloudflare Workers?
response = await cache.match(cacheKey);
if (!response) {
// handle fetch data and cache
}
const myHeaders = new Headers();
myHeaders.set("Access-Control-Allow-Origin", event.request.headers.get("Origin"));
return new Response(JSON.stringify({
response
}), {
status: 200, headers: myHeaders
});
It's quite a pain actually. There is a sample from Cloudflare, but it can't be used directly. I finally worked it out recently, and put the detailed steps into a
blog post.
Here is the full code for the worker.
// Reference: https://developers.cloudflare.com/workers/examples/cors-header-proxy
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",
"Access-Control-Max-Age": "86400",
}
function handleOptions (request) {
// Make sure the necessary headers are present
// for this to be a valid pre-flight request
let headers = request.headers
if (
headers.get("Origin") !== null &&
headers.get("Access-Control-Request-Method") !== null &&
headers.get("Access-Control-Request-Headers") !== null
) {
// Handle CORS pre-flight request.
// If you want to check or reject the requested method + headers
// you can do that here.
let respHeaders = {
...corsHeaders,
// Allow all future content Request headers to go back to browser
// such as Authorization (Bearer) or X-Client-Name-Version
"Access-Control-Allow-Headers": request.headers.get("Access-Control-Request-Headers"),
}
return new Response(null, {
headers: respHeaders,
})
}
else {
// Handle standard OPTIONS request.
// If you want to allow other HTTP Methods, you can do that here.
return new Response(null, {
headers: {
Allow: "GET, HEAD, POST, OPTIONS",
},
})
}
}
async function handleRequest (request) {
let response
if (request.method === "OPTIONS") {
response = handleOptions(request)
}
else {
response = await fetch(request)
response = new Response(response.body, response)
response.headers.set("Access-Control-Allow-Origin", "*")
response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
}
return response
}
addEventListener("fetch", (event) => {
event.respondWith(
handleRequest(event.request).catch(
(err) => new Response(err.stack, { status: 500 })
)
);
});
Your code does not set the Content-Type header. You should either set it with myHeaders.set() or use the original response headers instead of creating an empty headers object:
response = await cache.match(cacheKey);
if (!response) {
// handle fetch data and cache
}
const myHeaders = new Headers(response.headers);
myHeaders.set("Access-Control-Allow-Origin", event.request.headers.get("Origin"));
I'm also questioning why you are trying to use JSON.stringify on the response. I think what you want is something like this:
return new Response(response.body, {status: 200, headers: myHeaders});
Here's how got it working. I'm using Itty Router but this should work the same for a normal worker as well.
router.get("/data", async (request) => {
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "*",
};
if (request.method.toLowerCase() === "options") {
return new Response("ok", {
headers: corsHeaders,
});
}
try {
const data = await getData();
return new Response(data, {
headers: {
"Content-Type": "application/json",
// don't forget this.👇🏻 You also need to send back the headers with the actual response
...corsHeaders,
},
});
} catch (error) {}
return new Response(JSON.stringify([]), {
headers: corsHeaders,
});
});
Thanks to this Free Egghead video Add CORS Headers to a Third Party API Response in a Workers API
Anurag's answer got me 90% of the way there, thank you for that. I'm using itty-router as well, here's what I had to add.
I created a simple preflight middleware:
const corsHeaders = {
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Origin': '*',
};
export const withCorsPreflight = (request: Request) => {
if (request.method.toLowerCase() === 'options') {
return new Response('ok', {
headers: corsHeaders,
});
}
};
I then added it to every route.
router.all('*', withCorsPreflight);
router.get('/api/another-route', handler);
Hope this helps!
Related
I'm invoking a supabase edge function with the following
async function getData(plan_data){
console.log(plan_data)
console.log(JSON.stringify({plan_data}))
const { data, error } = await supabase.functions.invoke("create-stripe-checkout",
{
body: JSON.stringify({
plan_data
}),
}
)
console.log(data, error)
// console.log(data)
}
In the edge function I console logged the request and it stated bodyUsed: false. Essentially the edge function acts like and believes that no value was passed. (A value is passed to the getData function properly).I've played around with the syntax a bit to no avail, am I missing something?
EDIT:
Edge function is as follows
import { serve } from "https://deno.land/std#0.131.0/http/server.ts"
serve(async (req) => {
if (req.method === "OPTIONS"){
return new Response (null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "apikey, X-Client-Info, Authorization, content-type",
}
})
}
console.log(req)
const { planId } = await req.json()
console.log(planId)
return new Response(
JSON.stringify({ planId }),
{ headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "apikey, X-Client-Info, Authorization, content-type",
// "Content-Type": "application/json",
} },
)
})
EDIT: I tried running it with supabase's example code and had the same issue.
Seeing how this was a CORS error, I ended up allowing all headers in the preflight check response CORS headers.
I.e.
...
if (req.method === "OPTIONS"){
return new Response (null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*", // <-- change here
}
})
}
...
Does adding the content type header work?
async function getData(plan_data) {
console.log(plan_data)
console.log(JSON.stringify({ plan_data }))
const { data, error } = await supabase.functions.invoke("create-stripe-checkout",
{
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
plan_data
}),
});
console.log(data, error)
}
Supabase SDK will take care of json encoding, so you don't need to do it by yourself.
async function getData(plan_data){
const { data, error } = await supabase.functions.invoke("create-stripe-checkout",
{
body: { plan_data },
}
)
console.log(data, error)
}
You should be able to get the data on your Edge Function like this:
const { plan_data } = await req.json()
console.log(plan_data)
So I have a service worker for fetch:
self.addEventListener('fetch', (event) => {
const requestProcessor = (idToken) => {
let req = event.request;
// For same origin https requests, append idToken to header.
if ((self.location.protocol === 'https:' ||
self.location.hostname === 'localhost') &&
idToken) {
// Clone headers as request headers are immutable.
const headers = new Headers();
for (let entry of req.headers.entries()) {
headers.append(entry[0], entry[1]);
}
// Add ID token to header.
headers.append('Authorization', self.location.origin === getOriginFromUrl(event.request.url) ? `Bearer ${idToken}` : idToken);
try {
req = new Request(req.url, {
method: req.method,
headers: headers,
mode: self.location.origin === getOriginFromUrl(event.request.url) ? 'same-origin' : req.mode,
credentials: req.credentials,
cache: req.cache,
redirect: req.redirect,
referrer: req.referrer,
body: req.body,
bodyUsed: req.bodyUsed,
context: req.context
});
} catch (e) {
console.error(e);
}
}
return fetch(req);
};
event.respondWith(getIdToken().then(requestProcessor));
});
It is being called in another file like so:
export const makePostRequest = (url = '', params = {}) => {
return fetch(url, {
method: 'POST',
body: JSON.stringify(params),
headers: {
'Content-type': 'application/json'
}
}).then((res) => res).catch((err) => console.log(err));
};
For some reason, the req.body is always undefined inside of the service worker. Furthermore, it looks like the fetch request happens twice. When I put a breakpoint and step through the code, I can see that nothing from the fetch is being picked up by the service worker. I don't understand.
Okay, so this isn't obvious. So after some research this solved my issue:
self.addEventListener('fetch', (event) => {
if (getOriginFromUrl(event.request.url) === 'https://app.example.com') {
const requestProcessor = (idToken) => {
let newRequest = null;
// For same origin https requests, append idToken to header.
if ((self.location.protocol === 'https:' || self.location.hostname === 'localhost') && idToken) {
try {
newRequest = new Request(event.request, {
headers: new Headers({
...event.request.Headers,
'Content-Type': 'application/json',
Authorization: 'Bearer ' + idToken,
})
})
} catch (e) {
console.log(e);
}
}
return fetch(newRequest);
};
/* Fetch the resource after checking for the ID token.
This can also be integrated with existing logic to serve cached files
in offline mode.*/
event.respondWith(getIdToken().then(requestProcessor, requestProcessor));
}
});
I also had to set the mode:
export const makePostRequest = (url = '', params = {}) => {
return fetch(url, {
method: 'POST',
mode: 'cors',
headers: {
'Content-type': 'application/json'
},
body: JSON.stringify(params)
}).then((res) => res).catch((err) => console.log(err));
};
There were two issues:
By default the header's mode was set to no-cors. According to a previous SO answer, I had to set the mode to cors to allow for non-basic headers which would also include body.
The other issue had to do with the headers being immutable. This had to be changed to copy properly.
The Request object will implement methods like .blob().
await req.blob()
How to update an axios instance while intercept a response using data from this response without second request? New token can be received in any response after any request. Last received token should be used in any new request.
const request = (axios as any).create({
baseURL: mainConfig.apiBaseUrl,
headers: {
'Content-Type': 'application/json',
},
});
// Check token relevance and update if not relevance.
request.interceptors.response.use(response => {
if (response.headers.token !== undefined) {
response.config.headers.token = response.headers.token;
}
return response;
});
Here, you can't set it for further requests like this.
You should use globalStorage or any other store for storing this token.
import Store from "Store";
const {token} = Store;
const request = (axios as any).create({
baseURL: mainConfig.apiBaseUrl,
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
});
// Set token
request.interceptors.response.use(response => {
const {token} = response.headers;
if (token) {
Store.setToken(token);
}
return response;
}
I'm trying to configure the backend API on my app and here's the code to send a request:
static async xhr(endpoint, args, method) {
const url = `${API_SERVER}${endpoint}`;
let formBody = [];
for (let property in args) {
let encodedKey = encodeURIComponent(property);
let encodedValue = encodeURIComponent(args[property]);
formBody.push(encodedKey + "=" + encodedValue);
}
formBody = formBody.join("&");
let options = Object.assign({ method: method }, args ? { body: formBody } : null );
try {
let headers = {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
};
accessToken = 'bdi8HD8js91jdoach7234h';
if(accessToken != null)
headers['Authorization'] = accessToken;
options.headers = headers;
return fetch(url, options).then( resp => {
console.log(resp);
let json = resp.json();
if(resp.status >= 200 && resp.status < 300) {
if (resp.ok) {
return json
}
} else {
return {};
}
return json.then(err => {throw err});
});
} catch (error) {
return null;
}
}
Note: I debugged and found that the headers are correctly getting added to the options variable, but for some reason, the server isn't receiving the Authorization header.
I used Postman to send the exact same request with the exact same headers and I'm getting the correct response via it. I have no idea what's wrong, except it would only be so if the headers aren't getting sent in the first place.
Can someone please tell me what am I doing wrong? Thanks!
The headers option has to be an instance of Headers. You can transform your current headers object to a Headers instance by passing it to its constructor like this:
const headers = new Headers({
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
});
Note that I also replaced let with const since that variable is not going to be reassigned.
To change a header or add a new header to that Headers instance, you can use the set method. Instead of headers['Authorization'] = accessToken you'd do ...
headers.set('Authorization', accessToken)
I'm trying to get file uploading working with Express, but every multipart/form-data request I send gets a 400 bad request response with no error. Just an empty object. I'm currently using busboy-body-parser for parsing multipart formdata requests, but I also tried express-fileupload and ran into the exact same issue.
Here are my request methods:
get(endpoint) {
return this.request('GET', endpoint, null);
}
post(endpoint, body) {
return this.request('POST', endpoint, body);
}
postFile(endpoint, file) {
return this.request('POST', endpoint, file, contentTypes.file);
}
async request(method, endpoint, body, contentType=contentTypes.json) {
const { authToken } = this;
const endpointUrl = this.getEndpointUrl(endpoint);
const headers = new Headers();
if(authToken) {
headers.append(authTokenHeader, authToken);
}
headers.append('Accept', 'application/json, application/xml, text/plain, text/html, *.*');
headers.append('Content-Type', contentType);
const response = await fetch(endpointUrl, {
method: method,
headers: headers,
body: this._serializeRequestBody(body, contentType),
});
const result = await this._parseResponse(response);
if(!response.ok) {
if(response.status === 401) {
this.revokeAuth();
}
throw result || new Error('Unknown error (no error in server response)');
} else if(result && result.authToken) {
this.setAuthToken(result.authToken);
}
return result;
}
Here is _serializeRequestBody and _parseResponse:
_parseResponse(response) {
const contentType = response.headers.get('Content-Type').split(';')[0];
if(contentType === contentTypes.json) {
return response.json();
}
return response.text();
}
_serializeRequestBody(body, contentType) {
if(!body) return null;
switch(contentType) {
case contentTypes.json:
return JSON.stringify(body);
case contentTypes.file:
const formData = new FormData();
formData.append('file', body);
return formData;
}
return body;
}
And contentTypes:
const contentTypes = {
json: 'application/json',
file: 'multipart/form-data',
};
And my express middleware:
if(this._expressLoggingMiddleware) {
app.use(this._expressLoggingMiddleware);
}
if(this.isNotProductionEnv && this.isNotTestEnv) {
app.use(require('morgan')('dev'));
}
// Adds `res.success` and `res.fail` utility methods.
app.use(require('./utils/envelopeMiddleware')());
// Allow cross-origin requests if `config.cors` is `true`.
if(config.cors) app.use(require('cors')());
// Parse JSON and form request bodies.
app.use(busboyBodyParser()); // parses multipart form data (used for file uploads)
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Lookup users by the JWT in their request header.
app.use(passportJWTMiddleware(passport, this.jwtSecret, jwtPayload =>
this.lookupUserfromAuthToken(jwtPayload).catch((error) => {
log.warn('Error while looking up user from JWT:', error);
return null;
})
));
// Serve files from `config.publicDir`.
if(config.publicDir) {
log.debug(`Hosting static files in "${config.publicDir}"`);
app.use(express.static(config.publicDir));
}
Here is the request info in Chrome's dev tools
And the request payload:
And the response:
The answer ended up being: don't manually set the Content-Type header to multipart/form-data because if you let the browser do it for you, it will also tack on the required boundary value to the content type.
So in order to fix my code, I just have to only set the Content-Type header explicitly when sending JSON:
if(contentType === contentTypes.json) headers.append('Content-Type', contentType);