Bad request response to fetch REST API - javascript

I have built an API and app that uses that API. When I POST method via Postman, it works fine, but when I try fetching it via app, I get a bad request 400 status response. What am I doing wrong?
Here is my JavaScript code:
const myForm = document.getElementById('loginForm');
myForm.addEventListener('submit', function(e) {
e.preventDefault();
const url = 'https://thawing-peak-69345.herokuapp.com/api/auth';
const myHeaders = new Headers();
myHeaders.append('Accept', 'application/json, text/html, */* ');
myHeaders.append('Content-Type', 'application/json, charset=utf-8')
const formData = {
email: this.email.value,
password: this.password.value
};
console.log(formData);
const fetchOptions = {
method: 'POST',
mode: 'no-cors',
cache: 'no-cache',
headers: myHeaders,
body: JSON.stringify(formData)
};
fetch(url, fetchOptions)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.log(err))
})
Request
Response
Headers request:
Headers response:

You said:
mode: 'no-cors',
This is a declaration that you are not doing anything that requires permission be granted with CORS. If you try to do anything that does need permission, it will be silently ignored.
myHeaders.append( 'Content-Type', 'application/json, charset=utf-8')
Setting the Content-Type header to a value not supported by the HTML form element's type attribute requires permission from CORS. application/json is not such a value.
Consequently, the request is sent as text/plain.
Since it isn't marked as being JSON, the server throws a 400 error.
You need to:
Remove mode: 'no-cors',
Make sure that the service you are making the request to will use CORS to grant you permission (or to use a service on the same origin as the request).

Related

Accessing in Chrome POST response body from Cloudflare Worker

I am trying to read a response from a Cloudflare Worker API, I have not set anything sepcial about it and in Postman the request works as expected. I am guessing I am missing some header but I am not sure which one or even if that is the reason reading the reponses body is not working in browser.
This is how I fetch the data:
const url = `${process.env.SOME_URL}/v1/method?token=${TOKEN}`
const headers = new Headers();
headers.append("Content-Type", "application/json;charset=UTF-8");
const requestOptions = {
method: 'POST',
redirect: 'follow',
mode: 'no-cors',
body: JSON.stringify(body),
headers
// referrerPolicy: 'no-referrer',
};
return fetch(url, requestOptions)
.then(async (response) => {
// no body to be parsed
return response.json()
} )
.then(result => console.log(result))
.catch(error => console.log('error', error));
Looking forward to your answers, thank you.
This behaviour is due to the no-cors config of the fetch-api.
For more details read about opaque requests here:
What limitations apply to opaque responses?

Fetch PUT request gets Cache-Control header modified on Firefox and not Chrome

For the context, I'm creating presigned URL to upload to S3. For these requests the Cache-Control header has to be set to the value public, max-age=31536000, immutable.
I do the fetch with this code
fetch(
uploadUrl,
{
method: 'PUT',
cache: 'no-store',
headers: { 'Content-Type': contentType, 'Cache-Control': 'public, max-age=31536000, immutable' },
body: data
})
.then(response => {
if (response.ok) {
doneFunc(publicUrl);
} else {
failureFunc(response.status);
}
})
.catch(response => {
failureFunc(response.status);
});
With Chrome the PUT request is actually sent with the Cache-Control header set in the fetch call public, max-age=31536000, immutable
With Firefox the PUT request is sent with the Cache-Control header set to public, max-age=31536000, immutable, no-cache. Notice the addition of no-cache at the end. This addition makes my presigned URL not valid.
I tried by removing the cache parameter, by setting it to no-cache and to no-store. Firefox always adds something to the Cache-Control header.
Do you know a way to make Firefox behaves like Chrome and respect the headers I set?
Try using the Headers object to add headers.
const headers = new Headers();
headers.append('Content-Type', contentType);
headers.append('cache-control', 'public, max-age=31536000, immutable, no-store');
fetch(
uploadUrl,
{
method: 'PUT',
headers: headers,
body: data
})
.then(response => {
if (response.ok) {
doneFunc(publicUrl);
} else {
failureFunc(response.status);
}
})
.catch(response => {
failureFunc(response.status);
});
My sample fetch request (if you want to test in firefox console)
const headers = new Headers();
headers.append('Content-Type', 'text/json');
headers.append('cache-control', 'public, max-age=31536000, immutable, no-custom');
const options = {
method: 'PUT',
headers: headers,
body: JSON.stringify({})
};
fetch('https://www.mozilla.org/', options)
.then(response => console.log(response.ok ? 'done' : 'fail'))
.catch(response => console.log('fail catch'));
Colin Nicholson had the right answer for me, but it was buried in a comment, so I'm reposting it at the top level:
When the network tab is open in Safari or Firefox, they add an extra no-cache header which causes the presigned S3 URL PUT request to fail. Close the dev console and the upload should work.

Unable to access cookie from fetch() response

Even though this question is asked several times at SO like:
fetch: Getting cookies from fetch response
or
Unable to set cookie in browser using request and express modules in NodeJS
None of this solutions could help me getting the cookie from a fetch() response
My setup looks like this:
Client
export async function registerNewUser(payload) {
return fetch('https://localhost:8080/register',
{
method: 'POST',
body: JSON.stringify(payload),
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
});
}
...
function handleSubmit(e) {
e.preventDefault();
registerNewUser({...values, avatarColor: generateAvatarColor()}).then(response => {
console.log(response.headers.get('Set-Cookie')); // null
console.log(response.headers.get('cookie')); //null
console.log(document.cookie); // empty string
console.log(response.headers); // empty headers obj
console.log(response); // response obj
}).then(() => setValues(initialState))
}
server
private setUpMiddleware() {
this.app.use(cookieParser());
this.app.use(bodyParser.urlencoded({extended: true}));
this.app.use(bodyParser.json());
this.app.use(cors({
credentials: true,
origin: 'http://localhost:4200',
optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
credentials: true
}));
this.app.use(express.static(joinDir('../web/build')));
}
...
this.app.post('/register', (request, response) => {
const { firstName, lastName, avatarColor, email, password }: User = request.body;
this.mongoDBClient.addUser({ firstName, lastName, avatarColor, email, password } as User)
.then(() => {
const token = CredentialHelper.JWTSign({email}, `${email}-${new Date()}`);
response.cookie('token', token, {httpOnly: true}).sendStatus(200); // tried also without httpOnly
})
.catch(() => response.status(400).send("User already registered."))
})
JavaScript fetch method won't send client side cookies and silently ignores the cookies sent from Server side Reference link in MDN, so you may use XMLHttpRequest method to send the request from your client side.
I figured it out. The solution was to set credentials to 'include' like so:
export async function registerNewUser(payload) {
return fetch('https://localhost:8080/register',
{
method: 'POST',
body: JSON.stringify(payload),
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
});
}
After that I needed to enabled credentials in my cors middleware:
this.app.use(cors({
credentials: true, // important part here
origin: 'http://localhost:4200',
optionsSuccessStatus: 200
})
And then finally I needed to remove the option {httpOnly: true} in the express route response:
response.cookie('token', '12345ssdfsd').sendStatus(200);
Keep in mind if you send the cookie like this, it is set directly to the clients cookies. You can now see that the cookie is set with: console.log(document.cookie).
But in a practical environment you don't want to send a cookie that is accessible by the client. You should usually use the {httpOnly: true} option.

Fetch, set-cookies and csrf

I m using Isomorphic fetch in my application and I m having some troubles dealing with CSRF.
Actually, I m having a backend that sends me a CSRF-TOKEN in set-cookies property :
I have read somewhere that it's not possible, or it's a bad practice to access this kind of cookies directly inside of my code.
This way, I tried to make something using the credentials property of fetch request :
const headers = new Headers({
'Content-Type': 'x-www-form-urlencoded'
});
return this.fetcher(url, {
method: 'POST',
headers,
credentials: 'include',
body: JSON.stringify({
email: 'mail#mail.fr',
password: 'password'
})
});
This way, I m able to send my CSRF cookie back to my server to serve my need (it's a different one, because it s not the same request) :
My problem
My problem is that my backend needs to receive a x-csrf-token header and so I can't set it to my POST request.
What I need
How can I do to put the value of set-cookies: CSRF-TOKEN into the next request x-csrf-token header ?
It looks like in your scenario you are supposed to read from CSRF-TOKEN cookie. Otherwise it would be marked HttpOnly as JSESSIONID. The later means you cannot access it from the web page but merely send back to server automatically.
In general there is nothing wrong in reading CSRF token from cookies. Please check this good discussion: Why is it common to put CSRF prevention tokens in cookies?
You can read your cookie (not HttpOnly, of cause) using the following code
function getCookie(name) {
if (!document.cookie) {
return null;
}
const xsrfCookies = document.cookie.split(';')
.map(c => c.trim())
.filter(c => c.startsWith(name + '='));
if (xsrfCookies.length === 0) {
return null;
}
return decodeURIComponent(xsrfCookies[0].split('=')[1]);
}
So fetch call could look like
const csrfToken = getCookie('CSRF-TOKEN');
const headers = new Headers({
'Content-Type': 'x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken
});
return this.fetcher(url, {
method: 'POST',
headers,
credentials: 'include',
body: JSON.stringify({
email: 'test#example.com',
password: 'password'
})
});
Yes header name depends on your server. For example django usecase to setup CSRF token using fetch is like this:
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json; charset=UTF-8',
'X-CSRFToken': get_token
},

Unable to fetch POST without no-cors in header

On making request like that:
return fetch(
'http://localhost:8000/login',
{ method: 'POST',
headers: new Headers(
{"Content-Type": "application/json",
"Accept":"application/json"}
),
body: JSON.stringify(
{'name': 'Tom', 'password': 'Soyer'}
)
}
).then( response => { console.log(response);})
.catch(err => console.log(err))
request running with method OPTIONS instead POST.
Only on adding mode: 'no-cors' request become POST:
return fetch(
'http://localhost:8000/login',
{ method: 'POST',
mode: 'no-cors',
headers: new Headers(
{"Content-Type": "application/json",
"Accept":"application/json"}
),
body: JSON.stringify(
{'name': 'Tom', 'password': 'Soyer'}
)
}
).then( response => { console.log(response);})
.catch(err => console.log(err))
but response not ok than (even if network response status is 200): {type: "opaque", url: "", status: 0, ok: false, statusText: ""…}
I suppose it because
The only allowed values for the Content-Type header are:
application/x-www-form-urlencoded multipart/form-data text/plain
described here https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS
Is any way bring to live POST json data with fetch?
The custom Content-Type header you're sending causes your request to be preflighted, which means an OPTIONS request, containing some metadata about the POST request that is about to be dispatched, will be sent before the actual POST request.
Your server needs to be prepared to deal with this OPTIONS request. You haven't specified what the server is written in, but with express for example, you can register a middleware that intercepts all 'OPTIONS' requests, sets the Access-Control-Allow-Origin: * and Access-Control-Allow-Headers: Content-Type headers, and responds with 200.
If it is possible for you to make the request using a 'Content-Type': 'text/plain' header, that would solve your problem. Alternatively you could use something that bypasses XHR entirely, like JSONP.
When using non-cors, all headers must be valid simple-headers. The only valid values for the content-type header that qualifies as a simple-header is:
headers: [
['Content-Type', 'application/x-www-form-urlencoded'],
['Content-Type', 'multipart/form-data'],
['Content-Type', 'text/plain'],
]
Exceptions with contingencies:
headers: [
['Content-Type', 'application/csp-report'],
['Content-Type', 'application/expect-ct-report+json'],
['Content-Type', 'application/xss-auditor-report'],
['Content-Type', 'application/ocsp-request'],
]
simple-header
cors-protocol-exceptions
If you are trying to call an api and getting stuck with this in your react app you can add a proxy to the server and the cors error will get removed
just add this line at the package.json
"proxy":"url-to-your-server",

Categories

Resources