Next.js

Set the request's mode to 'no-cors'? What to do instead

What the browser's no-cors suggestion really does to an authenticated API call, the one case where it is the right answer, and the two fixes that return your data.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

The sentence "If an opaque response serves your needs, set the request's mode to 'no-cors'" is an offer, not a fix. no-cors does not make the server allow you: it sends a stripped-down request and hands your script an opaque response with status 0 and an empty body. For an API that needs an Authorization header it is worse, because the header is silently dropped, the API answers 401, and your code cannot even see the 401. Leave the mode alone. Either the API sends Access-Control-Allow-Origin and allows your header in its preflight answer, or your page calls a route on its own origin that calls the API from the server.

The error from the Stack Overflow question, a React app on localhost:3000 calling Yahoo's weather endpoint:

text
Access to fetch at
'https://weather-ydn-yql.media.yahoo.com/forecastrss?location=sunnyvale,ca&format=json'
from origin 'http://localhost:3000' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check: 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.

Read the message in three parts#

The message has three clauses, and each one tells you something different.

"Response to preflight request doesn't pass access control check" means the browser never sent your GET. It first sent an OPTIONS preflight, because the request carried something that is not CORS-safelisted. MDN's CORS-safelisted request header list is short (Accept, Accept-Language, Content-Language, Content-Type with three MIME types, Range), so an Authorization header or any custom API-key header is enough to trigger the preflight. Without that clause, the request went out as a simple request and the real response came back without the header.

"No 'Access-Control-Allow-Origin' header is present" names what the server's answer lacked.

"If an opaque response serves your needs…" is generic advice that Chromium bolted onto fetch errors. In the Chromium source at tag 72.0.3626.121, the sentence is appended only when the initiator is fetch, never XMLHttpRequest (the file gates its other no-cors suggestions on fetch the same way):

text
      if (initiator_name == fetch_initiator_type_names::kFetch) {
        builder.Append(
            " If an opaque response serves your needs, set the request's "
            "mode to 'no-cors' to fetch the resource with CORS disabled.");
      }

It is still there at tag 135.0.7049.0 and gone from tag 136.0.7103.0 and from the main branch of cors_error_string.cc read on 24 October 2026. So if your console shows the same error without the last sentence, you have the same problem on a newer browser, not a different one.

Reproduce both versions of the error#

The scenarios below were reproduced on 24 October 2026 in the official Playwright v1.63.0 Docker image (Node v24.20.0, Chromium 153.0.8010.12, Firefox 155.0). A page on localhost:3000 calls a small weather API on port 8080 that sends no CORS headers and answers 401 when the Authorization header is missing. The API logs every request it receives.

With an Authorization header, Chromium sends a preflight and stops there:

text
--- chromium S1: cors GET with Authorization header to :8080 (no CORS headers)
[chromium console.error] Access to fetch at 'http://localhost:8080/forecast?location=london,uk&format=json' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
[chromium] fetch rejected: TypeError: Failed to fetch
text
[api:8080] OPTIONS /forecast?location=london,uk&format=json origin=http://localhost:3000 authorization=- access-control-request-headers=authorization content-type=- body=-

The preflight names the header it wants to send (access-control-request-headers=authorization), and the API never sees the GET. Remove the header and the preflight clause disappears from the message, because the GET now goes out directly:

text
--- chromium S2: cors GET without custom headers to :8080 (no CORS headers)
[chromium console.error] Access to fetch at 'http://localhost:8080/forecast?location=london,uk&format=json' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Note that Chromium 153 printed neither message with the no-cors sentence. Firefox words the same failure differently and reports the status of the response it refused to hand over:

text
[firefox console.error] [JavaScript Error: "Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:8080/forecast?location=london,uk&format=json. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing). Status code: 401."]

What taking the hint does to an authenticated request#

The Fetch Standard defines the no-cors request mode as one that "Restricts requests to using CORS-safelisted methods and CORS-safelisted request-headers. Upon success, fetch will return an opaque filtered response." Two consequences follow, and the reproduction shows both.

The Authorization header is dropped without any error. The same GET with mode: 'no-cors' and the header set in code reached the API like this:

text
[api:8080] GET /forecast?location=london,uk&format=json origin=- authorization=- access-control-request-headers=- content-type=- body=-

No preflight, no Origin and no Authorization. The API answered 401, and the script was told nothing about it:

text
--- chromium S3: no-cors GET with Authorization header to :8080
[chromium network] GET http://localhost:8080/forecast?location=london,uk&format=json -> 401
[chromium] response.type=opaque status=0 ok=false body=""

The console error is gone, which is why the hint looks like it worked. The data is gone too, and the 401 explaining why is visible only in the Network panel. Calling response.json() on that response throws a SyntaxError; that symptom has its own page: SyntaxError: Unexpected end of input with mode: 'no-cors', which also covers the Content-Type rewrite on POST bodies.

Methods are restricted too: the spec allows only GET, HEAD and POST in no-cors mode, and anything else throws before a request leaves the browser:

text
[chromium] fetch rejected: TypeError: Failed to execute 'fetch' on 'Window': 'PUT' is unsupported in no-cors mode.
[firefox] fetch rejected: TypeError: Window.fetch: Invalid request method PUT.

When an opaque response really serves your needs#

The hint is honest about one case: you only need the request to arrive. A page-view ping or a fire-and-forget form post fits, as long as it uses a safelisted method and a plain-text body. In the reproduction, a no-cors POST to the API's ping route arrived intact, and the script got the usual opaque result:

text
[api:8080] POST /ping origin=http://localhost:3000 authorization=- access-control-request-headers=- content-type=text/plain;charset=UTF-8 body=page=/checkout
text
--- chromium S5: no-cors POST ping (fire and forget) to :8080
[chromium network] POST http://localhost:8080/ping -> 204
[chromium] response.type=opaque status=0 ok=false body=""

This is the same kind of request the browser already makes for markup. MDN's Request.mode page notes that img, script (except modules), link and similar elements load in no-cors mode unless the crossorigin attribute is present. If you need to read the JSON, you are not in this case.

Fix#

1. If you control the API: allow the origin and the header#

The preflight answer must allow your origin and the header that triggered it. In plain node:http, the weather API becomes:

js
import http from 'node:http';
 
const corsHeaders = {
  'Access-Control-Allow-Origin': 'http://localhost:3000',
  'Access-Control-Allow-Methods': 'GET, OPTIONS',
  'Access-Control-Allow-Headers': 'Authorization',
};
 
http.createServer((req, res) => {
  if (req.method === 'OPTIONS') {
    res.writeHead(204, corsHeaders);
    return res.end();
  }
  if (!req.headers.authorization) {
    res.writeHead(401, { 'Content-Type': 'application/json', ...corsHeaders });
    return res.end(JSON.stringify({ error: 'missing Authorization header' }));
  }
  res.writeHead(200, { 'Content-Type': 'application/json', ...corsHeaders });
  res.end(JSON.stringify({ location: 'london,uk', forecast: [{ day: 'Sat', high: 14, text: 'Showers' }] }));
}).listen(8081);

This file was run as printed, and Chromium called it from the page on port 3000 with and without the token:

text
[snippet with token] response.type=cors status=200 ok=true body="{\"location\":\"london,uk\",\"forecast\":[{\"day\":\"Sat\",\"high\":14,\"text\":\"Showers\"}]}"
[snippet without token] response.type=cors status=401 ok=false body="{\"error\":\"missing Authorization header\"}"

Note the CORS headers on the 401 as well: that is why the second call returns a readable 401 with its JSON reason. Without them, as in the Firefox log above, a missing token surfaces as a CORS error instead. Framework-specific setups (Express cors(), credentials, why * is refused with cookies) are covered in Fix: blocked by CORS policy from origin localhost:3000.

2. If you don't control the API: call it from your own server#

You cannot add your origin to an API you don't run. A comment by goto1, quoted in the top answer to the question, puts it plainly: "If the API endpoint is not something that you control, then you have to proxy your request through a server that does have CORS enabled." In Next.js, that server is a Route Handler, and it has a second benefit: the API token stays on the server instead of shipping in your bundle.

js
// app/api/weather/route.js
export async function GET(request) {
  const { searchParams } = new URL(request.url);
  const location = searchParams.get('location') ?? 'london,uk';
 
  const upstream = await fetch(
    `${process.env.WEATHER_API_URL}/forecast?location=${encodeURIComponent(location)}&format=json`,
    { headers: { Authorization: `Bearer ${process.env.WEATHER_API_TOKEN}` } },
  );
 
  const body = await upstream.text();
  return new Response(body, {
    status: upstream.status,
    headers: { 'Content-Type': upstream.headers.get('content-type') ?? 'application/json' },
  });
}

The page then calls its own origin, with no mode and no token:

js
const response = await fetch('/api/weather?location=london,uk');
const weather = await response.json();

In the reproduction, this handler was imported and run by the Node server of the page origin (not inside a running Next.js app), against the API with no CORS headers. The upstream call arrived with the token, and both browsers read the JSON:

text
[api:8080] GET /forecast?location=london%2Cuk&format=json origin=- authorization=Bearer demo-token access-control-request-headers=- content-type=- body=-
text
--- chromium S7: same-origin GET /api/weather (server-side route handler)
[chromium network] GET http://localhost:3000/api/weather?location=london,uk -> 200

Server-side fetch does not apply CORS, which is why the same API that blocked the browser answers the route.

Verify the fix#

Log response.type and response.status next to the call. With fix 1, the API's own log shows the preflight and then the real GET carrying the token, and the response type in code is cors:

text
[api:8081] OPTIONS /forecast?location=london,uk&format=json origin=http://localhost:3000 authorization=- access-control-request-headers=authorization content-type=- body=-
[api:8081] GET /forecast?location=london,uk&format=json origin=http://localhost:3000 authorization=Bearer demo-token access-control-request-headers=- content-type=- body=-
text
[chromium] response.type=cors status=200 ok=true body="{\"location\":\"london,uk\",\"forecast\":[{\"day\":\"Sat\",\"high\":14,\"text\":\"Showers\"}]}"

With fix 2 there is no preflight at all, only a same-origin GET, and the response type is basic:

text
[chromium] response.type=basic status=200 ok=true body="{\"location\":\"london,uk\",\"forecast\":[{\"day\":\"Sat\",\"high\":14,\"text\":\"Showers\"}]}"

If you still see type opaque and status 0, a mode: 'no-cors' is left somewhere in the call, or in a shared fetch wrapper.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

Check your own preflight message#

Paste the full console message into the explainer below to see which clause applies to your request. It runs in your browser; nothing is sent.

Everything runs in your browser — nothing you paste is sent anywhere, and tokens, cookies and passwords are masked in the output.

Optional: response headers (curl -i / curl -v output)
Try:

The extension answer, and redirects to a login page#

The second answer on the question recommends a browser extension that rewrites CORS headers. It does make the error disappear, but only in the browser where it is installed, so it is a local debugging aid and never a fix for your users.

One more case produces a close variant of this message, with a (redirected from …) clause and no preflight clause, and a hint you cannot use: a fetch whose request is redirected to an OAuth sign-in page. There, neither no-cors nor server headers help, because the page must navigate instead of fetching; see Access to fetch at accounts.google.com blocked by CORS. If, after removing no-cors, the error changes to a CORS message about credentials or a wildcard origin, continue with the localhost:3000 CORS walkthrough, which covers that next step. The short version: the no-cors sentence describes what the browser is willing to give you without the server's permission, which is almost never what an API call needs.

Further reading: Create React App's CORS Header Goes on the API, Not the App.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote