kit
kit copied to clipboard
fetch() unexpected behavor when encountering a CORS issue on client side
Describe the bug
If SvelteKit's fetch()
is called against a URL that causes a CORS error in a web browser, the resulting behaviour might be unexpected for a developer. The behaviour is also naturally different on client-side and server-side rendering, where SSR fetch()
does not suffer from CORS issues.
- A CORS error does not return a response in
fetch()
but throws an exception. Developers relying onresponse.ok
pattern to check the validity of thefetch
result are caught off guard. - This is a tough nut to crack, because it is rooted deeply in the web browser security model.
- The behaviour is unexpected because you need to work around using
try ... catch
instead ofresponse.ok
attribute to check the error. - Although something must "have gone wrong before" this occurs, the unexpected behaviour is common, because many times HTTP 404 or HTTP 500 responses lack CORS headers on them. If a developer naively checks for
response.status === 404
the check might fail in a web browser, assuming a low quality third party API. This can happen if a developer is usingfetch()
inload()
,get()
or similar with third-party APIs. This is especially bad, when SSR renders different pages (w/working error handling) than the client-side router.
Reproduction
This issue cannot be demostrated on StackBlitz (node.new) because it is related to low-level network primitives.
Here is an example one can throw in their load()
function:
try {
// causes CORS error because lack of CORS headers in 404
resp = await fetch("https://example.com/random");
} catch(e) {
// A web browser throws TypeError: Failed to fetch
// but happens only on client-side.
// The exception is hard to distinguish from
// any other exception, because it lacks metadata
// (class name, attributes).
console.error("Received", e)
// Temporary workaround
resp = {ok: false, statusText: e.message};
}
let error, meta;
if (resp.ok) {
meta = await resp.json();
error = null;
} else {
meta = {};
error = resp.statusText;
}
Logs
You will see this in the web developer console:
Access to fetch at 'https://example.com/random' from origin 'http://localhost:3000' 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.
However I am not sure if this information is accessible within client-side JavaScript.
System Info
System:
OS: macOS 12.3.1
CPU: (10) arm64 Apple M1 Max
Memory: 902.50 MB / 64.00 GB
Shell: 5.8.1 - /opt/homebrew/bin/zsh
Binaries:
Node: 16.14.2 - ~/.nvm/versions/node/v16.14.2/bin/node
Yarn: 1.22.15 - ~/.nvm/versions/node/v16.14.2/bin/yarn
npm: 8.7.0 - ~/.nvm/versions/node/v16.14.2/bin/npm
Browsers:
Chrome: 101.0.4951.64
Firefox: 100.0.2
Safari: 15.4
npmPackages:
@sveltejs/adapter-auto: next => 1.0.0-next.34
@sveltejs/kit: next => 1.0.0-next.308
svelte: ^3.46.0 => 3.46.6
Severity
annoyance
Additional Information
I am not sure what would be the best way to address the issue.
If this issue is not fixable on a framework level, I suggest this issue is highlighted in
- tutorial
- examples
- documentation
- etc.
and developers are educated that fetch()
may throw an exception, besides returning error responses.
There are likely to be other similar low-level network issues in a web browser that needs to be handled via catch in a fetch()
caller.
This would be a documentation thing, if anything. There's nothing SvelteKit can do about CORS. If the fetch
is throwing an exception and JS in the site is unable to see what the error response is to its request, SvelteKit's not going to be able to see it either. I don't think SvelteKit should try to catch this error on its own and either try to replace it with a different error or replace it with a fake Response
with !resp.ok
.
I am happy to tackle this. Is there anything special needed for doc updates - like discussing with someone beforehand?
Could SvelteKit's fetch
wrapper not intercept the response and see if it would throw if it happened in a browser? Presumably if we make a request to a different origin, we just need to check the Access-Control-Allow-Origin
header in the returned response a) exists and b) is either equal to *
or event.url.origin
.
If mode === 'no-cors'
we'd also need to proxy the response object so that we can replace the body with an empty string, but that also feels doable.
We seem to get this new simulated CORS error a lot on netlify deploys. Not sure if there's some caching going on or if the render-function sets the origin to different values. I'll try to figure out what's going on, but if somebody has a pointer I'm all ears ;)
When testing sveltekit on localhost with an API on example.com this will now throw an error. Is there a way stop simulating the error for localhost?
Same problem here. I can get the data I want in a local console, and it will render to the DOM for a split second until I get hit with the same error previously described.
Same here - started getting this error recently
Same problem here. I can get the data I want in a local console, and it will render to the DOM for a split second until I get hit with the same error previously described.
UPDATE: passing "&origin=*" into my API call fixed my issue. Weird as this was working without it prior to sveltekit update.
Same problem here. When using an AWS AppSync won't return a CORS header from a SSR fetch call, it does work in the browser. I want to opt-out this check.
There was honestly not much value in having SvelteKit "stimulate" CORS, now people need to worry about CORS issues in both the browser AND SvelteKit, which is often not straightforward.