Home/Development/Jupyter CORS Error: Fix 'No Access-Control-Allow-Origin' in 5 Minutes
DevelopmentAccess-Control-Allow-Originpreflight OPTIONS

Jupyter CORS Error: Fix 'No Access-Control-Allow-Origin' in 5 Minutes

Diagnose CORS errors in Jupyter Notebook/Lab and Colab plus 'No Access-Control-Allow-Origin' in 5 minutes: ServerApp.allow_origin config, copy-paste Express, Spring, and Nginx code, and preflight pitfalls.

Jupyter CORS Error: Fix 'No Access-Control-Allow-Origin' in 5 Minutes
Contents
  1. Start by Dissecting the Error Message
  2. 5-Minute Diagnosis: Pinpoint Responsibility with the Network Tab
  3. Cause Table by Console Error Message
  4. Copy-Paste Fix Code by Server
  5. When You Get a CORS Error in Jupyter Notebook, JupyterLab, or Colab (allow_origin Config)
  6. Headers You Must Include on the Preflight Response
  7. Two Common Pitfalls
  8. Conclusion: Diagnosis → Fix Cheatsheet
  9. FAQ

TL;DR: CORS Errors Are Fixed on the Server (Response Headers)

Seeing a red error in the console and landed here from search? We'll skip the theory lecture and go straight to diagnosis. Three-line summary: CORS is a browser security policy that blocks requests sent to a different origin (protocol + domain + port). The key point: the headers that lift this block must be sent by the server (or Nginx in front of it), not the frontend. In other words, most CORS errors are a backend configuration problem. If the error comes from Jupyter Notebook, JupyterLab, or Colab, jump to the Jupyter section below for the allow_origin config.

Start by Dissecting the Error Message

This is the most common message.

CODE
Access to fetch at 'https://api.myapp.com/users' from origin 'https://myapp.com'
has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

Read it phrase by phrase and the diagnosis is done.

  • Access to fetch at 'https://api.myapp.com/users' → the target server the request was sent to
  • from origin 'https://myapp.com' → the origin (frontend) that sent the request
  • No 'Access-Control-Allow-Origin' header is present → the root cause: the allow header is missing from the server response

The last line is the conclusion. The server did not send Access-Control-Allow-Origin, so the place to fix is the backend. Note that even when this error appears, server logs often show the request arrived normally and returned 200. The response did arrive, but the browser discarded it instead of handing it to JS because the header was missing.

5-Minute Diagnosis: Pinpoint Responsibility with the Network Tab

Open DevTools → Network tab and click the blocked request. Use this checklist to decide in under 5 minutes.

① Do you see a preflight (OPTIONS) row? If an OPTIONS method row for the same URL appears first in the request list, a preflight occurred. Preflight is automatically attached under these conditions:

  • PUT, DELETE, PATCH methods
  • Content-Type: application/json
  • Adding a custom header such as Authorization

GET/POST (form-encoded) with none of the above is a simple request and goes through without OPTIONS.

② Is Access-Control-Allow-Origin in the Response Headers?

③ What is the status?

OPTIONS statusACAO headerVerdict
No OPTIONS + no ACAO on the actual request responseMissingServer issue — add header config
OPTIONS fails with 404/500-Server issue — OPTIONS route not handled
OPTIONS 200/204 but no ACAOMissingServer issue — preflight response headers missing
ACAO is * while using credentials*Pitfall (see trap ① below)
All headers look fine but still blockedPresentSuspect frontend credentials / origin typo

Bottom line: if the header is missing, the problem is on the server (or the proxy in front of it). Changing fetch options on the frontend will not fix it.

Cause Table by Console Error Message

Look up the exact English text from your console in the table below and it tells you what to fix.

Console error textCauseWhat to fix
No 'Access-Control-Allow-Origin' header is present on the requested resourceNo allow header in the responseAdd CORS headers on the server (or the proxy in front of it)
Response to preflight request doesn't pass access control checkThe OPTIONS response is 4xx/5xx or lacks CORS headersHandle OPTIONS before the auth filter and answer 204 with CORS headers
Method PUT is not allowed by Access-Control-Allow-Methods in preflight responseThe method isn't in the allowed listAdd it to Access-Control-Allow-Methods
Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight responseThe header isn't in the allowed listAdd it to Access-Control-Allow-Headers
... must not be the wildcard '*' when the request's credentials mode is 'include'* combined with cookie authName the origin (or reflect it after an allow-list check) + Access-Control-Allow-Credentials: true
The 'Access-Control-Allow-Origin' header contains multiple values '...', but only one is allowedThe proxy and the backend both add the headerAdd CORS headers in one layer only
Redirect is not allowed for a preflight requestThe preflight was redirected with 301/302 (trailing slash, http→https, etc.)Call the final URL directly, with no redirect

Redirects cause trouble for the actual request too. The 3xx response also needs CORS headers, and they are easy to lose when a proxy or framework generates the redirect. Where possible, have the frontend call the final URL directly.

Check the Origin against an allow-list, reflect it, and add Vary: Origin

If you allow several origins and also use cookies, compare the request's Origin with an allow-list and echo it back only on a match.

JavaScript
const allowList = new Set(['https://app.com', 'https://admin.app.com']);
function setCors(req, res) {
  const origin = req.headers.origin;
  if (allowList.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin); // not '*'
    res.setHeader('Access-Control-Allow-Credentials', 'true');
  }
  res.setHeader('Vary', 'Origin'); // the response varies by origin, so include Origin in the cache key
}

Without Vary: Origin, a CDN or proxy can serve a response meant for one origin to another, which is cache poisoning.

Copy-Paste Fix Code by Server

Express (cors middleware)

JavaScript
const cors = require('cors');

app.use(cors({
 origin: 'https://myapp.com', // when
 credentials: true, // / when
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 7200,                  // cache preflight for 2 hours (Chromium cap)
}));

The cors middleware automatically handles OPTIONS preflight with a 204 response. To allow multiple origins, pass an array or a function to origin.

Spring Boot

To open a specific controller only, use the annotation:

JAVA
@CrossOrigin(origins = "https://myapp.com", allowCredentials = "true")
@RestController
public class UserController { ... }

For a clean global setup, the Bean approach is recommended.

JAVA
@Configuration
public class CorsConfig {
  @Bean
  public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://myapp.com"));
    config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
    config.setAllowedHeaders(List.of("Content-Type","Authorization"));
    config.setAllowCredentials(true);
    config.setMaxAge(7200L);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
  }
}

If you use Spring Security, add http.cors(Customizer.withDefaults()) so the Bean above actually applies. Otherwise the filter chain will block OPTIONS first.

Nginx

Nginx
location /api/ {
    add_header Access-Control-Allow-Origin  "https://myapp.com" always;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
    add_header Access-Control-Allow-Credentials "true" always;
    add_header Access-Control-Max-Age 7200 always;

    if ($request_method = OPTIONS) {
 return 204; # preflight 204 when reply
    }

    proxy_pass http://backend;
}

Two things matter. Without the always flag, headers are not attached to 4xx/5xx responses, so CORS fires again in error cases. And OPTIONS should be cut off immediately with 204 and no body so it never reaches the backend.

FastAPI (CORSMiddleware)

Python
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.com", "https://admin.app.com"],  # use an explicit list with credentials
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
    allow_headers=["Content-Type", "Authorization"],
    max_age=600,
)

Don't combine allow_credentials=True with allow_origins=["*"]. With that combination Starlette (which FastAPI is built on) doesn't raise an error; it reflects the request's Origin on credentialed requests and preflights, which ends up allowing credentialed requests from every site. The FastAPI docs also say to list the allowed origins explicitly when you use credentials.

When You Get a CORS Error in Jupyter Notebook, JupyterLab, or Colab (allow_origin Config)

First, figure out which side is the browser request. Calling an external API with Python requests from a notebook cell is a request sent by the kernel (server), not the browser, so CORS does not apply. You only get a CORS error in these two cases:

  1. Browser → external API: a %%javascript cell, ipywidgets/JupyterLab extension, or HTML inside the notebook calls another domain's API with fetch → the place to fix is that API server. Use the Express/Spring/Nginx code above to allow the Jupyter origin (e.g. http://localhost:8888). For causes by exact console error text, see the cause table above.
  2. External web app → Jupyter server: a React app (http://localhost:3000), a Google Colab local runtime, etc. calls Jupyter's REST API/WebSocket → the place to fix is the Jupyter server config. If the server log shows Blocking Cross Origin API request for /api/... Origin: ..., Host: ..., this is your case.

JupyterLab 3+ / Notebook 7 (Jupyter Server)

JupyterLab (since 3.0) and Notebook (since 7) run on Jupyter Server (jupyter_server). So the server option prefix is ServerApp and the config file is jupyter_server_config.py.

Bash
# Generate the config file: ~/.jupyter/jupyter_server_config.py
jupyter server --generate-config
Python
# ~/.jupyter/jupyter_server_config.py
c.ServerApp.allow_origin = "http://localhost:3000"   # exactly one allowed origin
c.ServerApp.allow_credentials = True                  # only for requests with cookies/auth

# Need several origins? Use a regex (ignored if allow_origin is set)
# Always anchor it with ^ and $
# c.ServerApp.allow_origin_pat = r"^https://(app|admin)\.example\.com$"

For a one-off run, command-line flags work too.

Bash
jupyter lab --ServerApp.allow_origin='http://localhost:3000' --ServerApp.allow_credentials=True

Classic Notebook (6.x and earlier)

The classic notebook uses the NotebookApp prefix and a different config file.

Bash
jupyter notebook --generate-config   # ~/.jupyter/jupyter_notebook_config.py
Python
# ~/.jupyter/jupyter_notebook_config.py
c.NotebookApp.allow_origin = "http://localhost:3000"
c.NotebookApp.allow_credentials = True
# c.NotebookApp.allow_origin_pat = r"^https://app\.example\.com$"

On Notebook 7, server options given with the old NotebookApp prefix are passed to ServerApp by the compatibility layer (notebook_shim) with a "has moved from NotebookApp to ServerApp" warning. For new setups, use ServerApp.

Google Colab Local Runtime

Colab's official Local runtimes doc starts a Jupyter runtime with this command, which explicitly trusts the Colab frontend origin:

Bash
jupyter notebook \
  --NotebookApp.allow_origin='https://colab.research.google.com' \
  --port=8888 \
  --NotebookApp.port_retries=0 \
  --NotebookApp.allow_credentials=True

Once the server starts, paste the printed http://localhost:8888/?token=... URL into the dialog from Colab's "Connect" button → "Connect to local runtime...". On Notebook 7, these NotebookApp flags are forwarded to ServerApp via notebook_shim (with a warning). Colab also offers a Docker runtime image; in that case, connect with the URL the container prints.

Tip: If nothing changed after editing the config, check ① that you used the right prefix (ServerApp vs NotebookApp) and config file for your version, ② that you restarted the server, and ③ whether the error text contains No 'Access-Control-Allow-Private-Network' header was present in the preflight response. If it is ③, the problem is not allow_origin but Chrome's Private Network Access restriction on public sites calling localhost (Colab issue googlecolab/colabtools#5506).

⚠️ Security: never use allow_origin = '*' in production. A Jupyter server grants arbitrary code execution to whoever connects. * allows any website, which opens a path for requests to your notebook API through the user's browser. Specify allowed origins exactly, and anchor allow_origin_pat with ^...$. In jupyter-server 2.17.0 and earlier, an unanchored pattern could let a domain like trusted.example.com.evil.com pass (advisory GHSA-24qx-w28j-9m6p, patched in 2.18.0). Do not disable token/password auth, and put the server behind an HTTPS reverse proxy when exposing it. Kernels talk over WebSocket, so the proxy needs WebSocket upgrade settings (see Nginx Reverse Proxy Config: Copy-Paste Examples for Routing API, Admin, and Frontend on One Domain).

Headers You Must Include on the Preflight Response

OPTIONS preflight is the step that asks "is it OK to send the actual request?" The browser decides whether to proceed based on this response.

  • Access-Control-Allow-Methods: allowed methods. Without it, PUT/DELETE get blocked
  • Access-Control-Allow-Headers: allowed custom headers. If Authorization is missing, token requests are blocked
  • Access-Control-Max-Age: how long to cache the preflight result, in seconds. Reduces OPTIONS firing before every request. Browsers cap it: Chromium honors at most 7200 seconds (2 hours) and Firefox 86400 seconds (24 hours); larger values are clamped to the cap (MDN)

OPTIONS does not receive data, so 204 with no body is the standard.

Two Common Pitfalls

① credentials: 'include' + wildcard (*) is forbidden

This combination blows up often with cookie-based auth. If the frontend sends:

JavaScript
fetch('https://api.myapp.com/me', { credentials: 'include' });

the moment the server returns Access-Control-Allow-Origin: *, you get this error.

CODE
The value of the 'Access-Control-Allow-Origin' header in the response
must not be the wildcard '*' when the request's credentials mode is 'include'.

Fix: specify a concrete origin instead of a wildcard, and pair it with Access-Control-Allow-Credentials: true. Same as the Express/Spring examples above. The cookie itself also needs SameSite=None; Secure to be sent cross-site, so handle those as a set.

A note from production: CORS issues exploded once we split frontend and API with a BFF and microservices. In my case, what ate the most time was not the CORS code but Spring Security filters blocking OPTIONS with 401 first. If OPTIONS is 401 in the Network tab, suspect the auth filter, not CORS config.

② Bypass locally with a dev proxy (not a production fix)

During local development, you can make the browser think it is the same origin via a proxy and bypass CORS entirely.

Vite (vite.config.js):

JavaScript
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
};

webpack devServer:

JavaScript
devServer: {
  proxy: { '/api': 'http://localhost:8080' },
}

The dev server forwards requests starting with /api to the backend, so from the browser's perspective it is same-origin. This does not apply to a production build. In production you must solve it with the server-side CORS config above.

Conclusion: Diagnosis → Fix Cheatsheet

  1. Read the error message — No Access-Control-Allow-Origin = confirmed server problem
  2. Network tab — judge frontend vs server from the OPTIONS row / ACAO header / status
  3. Add headers on the server — Express cors(), Spring CorsConfigurationSource, Nginx add_header ... always
  4. Handle preflight — OPTIONS returns 204, with Methods/Headers/Max-Age as a set
  5. If you use credentials — no wildcard; concrete origin + Allow-Credentials true
  6. Proxy locally, server config in production — never mix these up

One last action item. Do not spray Access-Control-Allow-Origin: * in production. The error disappears immediately, but you are opening authenticated APIs to any site. Explicitly whitelist allowed origins.

FAQ

Q. The server log shows 200, so why does the frontend still get a CORS error? A. The response arrived, but the browser blocked it just before handing it to JS because the Access-Control-Allow-Origin header was missing. The server can be working fine and you still get the error if only the header is absent. Add CORS response headers on the server.

Q. The OPTIONS request fails with 401/404. What should I do? A. You are blocked at the preflight stage. A Spring Security (or similar) auth filter is treating OPTIONS as an authenticated request, or the router has no OPTIONS handler. Exempt OPTIONS from auth and return 204.

Q. The local Vite proxy works, but CORS blows up again after deploy. A. The dev proxy only runs on the development server and is not included in the build output. In production you must set CORS headers directly on the backend or Nginx.

Verification
✦ ✦ ✦
Sources and verification

Sources and checks vary by article. Only articles with a recorded check date, scope and sources below were checked, and only within that scope. For other articles, verify against the current docs for your version before applying anything, and email us about errors. Editorial policy · email us

This article has no recorded human check (date and scope) yet.

Published · ·Updated ·
Topic-related official docs (auto-linked · not per-claim sources)NGINX documentation ↗Oracle Java documentation ↗Spring Boot documentation ↗

Comments

Be the first to comment.