Geonode logo
Website and server errors

How to Fix a 401 Unauthorized Error

Read what the site's challenge asks for, then fix the token, the redirect or the header, with copy-ready code.

Updated

TL;DR

401 Unauthorized means the website or API you called, not your proxy, refused the request because it carried no valid login or token. The fastest fix is to get a fresh token and send it in the scheme the response's WWW-Authenticate header names.

What a 401 looks like in each client

Most clients print only the status; the WWW-Authenticate header says what the site wants.

WhereWhat you see
Chrome, empty 401 body, below the heading This page isn't workingIf the problem continues, contact the site owner. HTTP ERROR 401
curl with --failcurl: (22) The requested URL returned error: 401
Python urlliburllib.error.HTTPError: HTTP Error 401: Unauthorized
Python requests, raise_for_status()requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://api.example.com/v1/items
axios (Node.js)AxiosError: Request failed with status code 401
API response header, expired OAuth tokenWWW-Authenticate: Bearer realm="example", error="invalid_token", error_description="The access token expired"

Why this happens

The site wants proof of who you are, and this request lacked a valid one.

Servers compare the Authorization header with the logins and tokens they issued. RFC 9110 makes them name the accepted scheme, such as Basic, Bearer or Digest, in a WWW-Authenticate header on every 401.

Diagnose your 401 first

Each check points to one cause, and the first needs only a browser.

  • Open the URL in Chrome; if a username and password dialog appears, the site wants an HTTP login such as Basic, not a token.

  • On a Bearer API, print WWW-Authenticate; error="invalid_token" means the token arrived but is expired, revoked or malformed, while a bare challenge usually means none arrived.

  • Run curl -v -L with your token; if a Location header moves you to another host before the 401, the token was dropped there.

  • Through a proxy, run curl -v; a 401 after CONNECT's 200 reply came from the site, while a CONNECT error reply came from the proxy.

  • Print the headers your code sends; if Authorization decodes to your proxy username, the site received the proxy login instead of its token.

Solutions ranked by effectiveness

Start with the token itself, then the route it travels.

  1. Most common fix

    Send the credential the challenge names

    Applies when the header is absent, mistyped or carries a stale value. Copy the format the API documents; for Bearer that is the word Bearer, one space, then the token, with no quotes.

    import urllib.error
    import urllib.request
    
    req = urllib.request.Request(
        "https://api.example.com/v1/items",
        headers={"Authorization": "Bearer TOKEN"},
    )
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            print(r.status, r.read().decode())
    except urllib.error.HTTPError as e:
        print(e.code, e.headers.get("WWW-Authenticate"))
  2. Check next

    Call the final address directly

    Applies when the first answer is a redirect. curl and Node.js fetch drop Authorization when the scheme, host or port changes; Python requests does too, except http to https on ports 80 and 443.

    1. Replace http:// with https:// in the URL your code calls.

    2. Match the final hostname exactly, with or without www, as the redirect shows.

    3. Use curl --location-trusted only when you trust every host in the chain.

  3. For developers

    Keep the proxy login out of Authorization

    Applies when you call the site through a proxy. Give the proxy its login in the proxy settings, sent as Proxy-Authorization; whatever sits in Authorization travels on to the site.

    import urllib.error
    import urllib.request
    
    proxy = "http://USERNAME:PASSWORD@proxy.geonode.io:9000"
    opener = urllib.request.build_opener(
        urllib.request.ProxyHandler({"http": proxy, "https": proxy}))
    req = urllib.request.Request("https://api.example.com/v1/items",
                                 headers={"Authorization": "Bearer TOKEN"})
    try:
        print(opener.open(req, timeout=30).status)
    except urllib.error.HTTPError as e:
        print(e.code, e.headers.get("WWW-Authenticate"))

Stop the 401 from coming back

Habits for scripts and integrations that call authenticated APIs every day.

  1. Turn each token's expires_in into a clock time on arrival, and refresh shortly before then.

  2. Retry a 401 once with new credentials; if the same challenge returns, stop and read the response body.

  3. Write the WWW-Authenticate value into your logs beside each 401, so the cause is on record without a rerun.

  4. Sign in to your own accounts through a sticky port, since some sites end a session when its IP changes.

One IP for your own loginsSticky sessions keep one IP across a login, from 3 minutes to 24 hours.
Try residential proxies

Related errors

Learn more

FAQ

It means the request reached the server without valid credentials for that page or API. If you did send a login or token, the server refused it, so send a new one rather than repeating the old.

Sign out and back in if a browser shows it. For a script, request a new access token, check that the header carries it in the documented format, and point the code at the final https:// address.

It is the API's verdict on your access token, relayed by the proxy untouched, since RFC 9110 forbids proxies to modify WWW-Authenticate. Ask the API's token endpoint for a new token; proxy settings play no part.

A 401 is the website asking for its own login in the Authorization header. A 407 is your proxy asking for its login in Proxy-Authorization before it forwards anything; Geonode's documented proxy errors include 407 but not 401.

curl -u sends Basic credentials unless told otherwise. If the server's challenge names Digest or NTLM, add --digest or --ntlm, or use --anyauth so curl reads the challenge first and picks the strongest scheme offered.

If your code signs the JWT, a drifting clock can put nbf or exp outside the server's leeway, which RFC 7519 says is usually a few minutes at most. GitHub asks apps to backdate iat 60 seconds and use NTP.

A separate login for the proxy

Geonode's dashboard gives the proxy its own username and password, apart from every site login.