<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: head_of_david</title>
    <description>The latest articles on DEV Community by head_of_david (@head_of_david).</description>
    <link>https://dev.to/head_of_david</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4188664%2Fc0de1076-c987-4141-b747-63d4cf36c0c8.png</url>
      <title>DEV Community: head_of_david</title>
      <link>https://dev.to/head_of_david</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/head_of_david"/>
    <language>en</language>
    <item>
      <title>Security Headers Checklist: 6 HTTP Headers Every Site Should Send</title>
      <dc:creator>head_of_david</dc:creator>
      <pubDate>Tue, 03 Nov 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/head_of_david/security-headers-checklist-6-http-headers-every-site-should-send-1pd1</link>
      <guid>https://dev.to/head_of_david/security-headers-checklist-6-http-headers-every-site-should-send-1pd1</guid>
      <description>&lt;p&gt;Security headers are the cheapest hardening you'll ever ship: a few lines of server config, no code changes, and whole classes of attack — clickjacking, MIME sniffing, protocol downgrades, much of XSS — get harder or impossible. Here are the six worth sending on every site, in the order that's safest to roll them out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Header&lt;/th&gt;
&lt;th&gt;Recommended value&lt;/th&gt;
&lt;th&gt;Stops&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.devkult.com/headers/x-content-type-options" rel="noopener noreferrer"&gt;X-Content-Type-Options&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nosniff&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Files being run as a type they weren't served as&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.devkult.com/headers/referrer-policy" rel="noopener noreferrer"&gt;Referrer-Policy&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;strict-origin-when-cross-origin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full URLs leaking to other sites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.devkult.com/headers/x-frame-options" rel="noopener noreferrer"&gt;X-Frame-Options&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DENY&lt;/code&gt; (or CSP &lt;code&gt;frame-ancestors 'none'&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Clickjacking via iframes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.devkult.com/headers/permissions-policy" rel="noopener noreferrer"&gt;Permissions-Policy&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;camera=(), microphone=(), geolocation=()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Injected code asking for powerful features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.devkult.com/headers/strict-transport-security" rel="noopener noreferrer"&gt;Strict-Transport-Security&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max-age=31536000; includeSubDomains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTPS → HTTP downgrades&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.devkult.com/headers/content-security-policy" rel="noopener noreferrer"&gt;Content-Security-Policy&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;depends on your site — start in report-only mode&lt;/td&gt;
&lt;td&gt;Most XSS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first four are nearly risk-free: they rarely break anything. HSTS and CSP need care, so they come last. To build all six at once for your server or host, use the &lt;a href="https://www.devkult.com/tools/security/security-headers-generator" rel="noopener noreferrer"&gt;security headers generator&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1–4: the safe ones
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt;&lt;/strong&gt; makes browsers trust your &lt;code&gt;Content-Type&lt;/code&gt; instead of guessing. The only thing it can break is a script or stylesheet served with the wrong type, which you want to fix anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Referrer-Policy: strict-origin-when-cross-origin&lt;/code&gt;&lt;/strong&gt; is already the default in current browsers; sending it makes the behaviour explicit for older ones. Use &lt;code&gt;no-referrer&lt;/code&gt; or &lt;code&gt;same-origin&lt;/code&gt; if your URLs carry anything sensitive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;X-Frame-Options: DENY&lt;/code&gt;&lt;/strong&gt; stops other sites from loading your pages in an iframe. Use &lt;code&gt;SAMEORIGIN&lt;/code&gt; if you frame your own pages. Once you have a CSP, its &lt;code&gt;frame-ancestors&lt;/code&gt; directive does the same job and is the modern replacement — send both for older browsers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Permissions-Policy&lt;/code&gt;&lt;/strong&gt; switches off browser features you don't use. List the ones that matter — camera, microphone, geolocation, payment — with an empty allowlist &lt;code&gt;()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5: HSTS, carefully
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Strict-Transport-Security&lt;/code&gt; tells browsers to use HTTPS for your domain, every time, for &lt;code&gt;max-age&lt;/code&gt; seconds — even if someone types &lt;code&gt;http://&lt;/code&gt;. It's powerful precisely because it can't be undone quickly: if a subdomain doesn't support HTTPS and you've sent &lt;code&gt;includeSubDomains&lt;/code&gt;, browsers will refuse to load it until the max-age runs out.&lt;/p&gt;

&lt;p&gt;Roll it out in steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;max-age=300&lt;/code&gt; — five minutes, to test.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max-age=86400; includeSubDomains&lt;/code&gt; — once you've confirmed every subdomain serves HTTPS.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max-age=31536000; includeSubDomains&lt;/code&gt; — a year, the usual production value.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;preload&lt;/code&gt; and submit the domain to hstspreload.org only if you're sure; removal takes months.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  6: CSP, in report-only mode first
&lt;/h2&gt;

&lt;p&gt;A Content-Security-Policy tells the browser which sources may provide scripts, styles, images and frames. A good one turns most injected-script XSS into a blocked request. It's also the header most likely to break your site, so start with &lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt;: the browser reports violations to the console (and to an endpoint, if you configure one) without blocking anything.&lt;/p&gt;

&lt;p&gt;A starting policy for a site that serves its own assets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Security-Policy: default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Third-party scripts, analytics and inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; blocks all need explicit allowances — the &lt;a href="https://www.devkult.com/blog/content-security-policy-guide" rel="noopener noreferrer"&gt;CSP guide&lt;/a&gt; covers nonces, hashes and the usual breakages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting them
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Next.js&lt;/strong&gt; (&lt;code&gt;next.config.js&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;securityHeaders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Content-Type-Options&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nosniff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Referrer-Policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;strict-origin-when-cross-origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Frame-Options&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DENY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Permissions-Policy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;camera=(), microphone=(), geolocation=()&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Strict-Transport-Security&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;max-age=31536000; includeSubDomains&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/(.*)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;securityHeaders&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;nginx&lt;/strong&gt; (inside the HTTPS &lt;code&gt;server&lt;/code&gt; block — &lt;code&gt;always&lt;/code&gt; adds them to error pages too):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Referrer-Policy&lt;/span&gt; &lt;span class="s"&gt;"strict-origin-when-cross-origin"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"DENY"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Permissions-Policy&lt;/span&gt; &lt;span class="s"&gt;"camera=(),&lt;/span&gt; &lt;span class="s"&gt;microphone=(),&lt;/span&gt; &lt;span class="s"&gt;geolocation=()"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Strict-Transport-Security&lt;/span&gt; &lt;span class="s"&gt;"max-age=31536000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;includeSubDomains"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One nginx trap: &lt;code&gt;add_header&lt;/code&gt; directives are inherited only if the inner block defines none of its own. A single &lt;code&gt;add_header&lt;/code&gt; inside a &lt;code&gt;location&lt;/code&gt; silently drops every header set at the &lt;code&gt;server&lt;/code&gt; level for that location.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Express&lt;/strong&gt;: the &lt;a href="https://helmetjs.github.io/" rel="noopener noreferrer"&gt;Helmet&lt;/a&gt; middleware sets sensible defaults for all of these, including a starter CSP — &lt;code&gt;app.use(helmet())&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vercel&lt;/strong&gt; (&lt;code&gt;vercel.json&lt;/code&gt;) and &lt;strong&gt;Netlify&lt;/strong&gt; (&lt;code&gt;_headers&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/(.*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"X-Content-Type-Options"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nosniff"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/*
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Headers you can drop
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;X-XSS-Protection&lt;/strong&gt; — the browser XSS filters it controlled have been removed. Send &lt;code&gt;0&lt;/code&gt; or nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Public-Key-Pins (HPKP)&lt;/strong&gt; — removed from browsers; it could lock users out of a site permanently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expect-CT&lt;/strong&gt; — obsolete now that Certificate Transparency is enforced by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature-Policy&lt;/strong&gt; — replaced by Permissions-Policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check your work
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sI&lt;/span&gt; https://your-site.example | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"strict-transport|content-security|x-content-type|referrer-policy|permissions-policy|x-frame"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To check many URLs at once, paste them into the &lt;a href="https://www.devkult.com/tools/web/http-status-checker" rel="noopener noreferrer"&gt;HTTP Status Checker&lt;/a&gt;. For every header's full set of values, see the &lt;a href="https://www.devkult.com/headers" rel="noopener noreferrer"&gt;HTTP headers reference&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.devkult.com/blog/security-headers-checklist" rel="noopener noreferrer"&gt;devkult.com&lt;/a&gt;. To build all six for your server or host in one go, use the free &lt;a href="https://www.devkult.com/tools/security/security-headers-generator" rel="noopener noreferrer"&gt;Security Headers Generator&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>nginx</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>CORS Errors Explained: How to Fix "No 'Access-Control-Allow-Origin' Header"</title>
      <dc:creator>head_of_david</dc:creator>
      <pubDate>Tue, 27 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/head_of_david/cors-errors-explained-how-to-fix-no-access-control-allow-origin-header-gdb</link>
      <guid>https://dev.to/head_of_david/cors-errors-explained-how-to-fix-no-access-control-allow-origin-header-gdb</guid>
      <description>&lt;p&gt;You call your API from the browser and the console fills with red: &lt;em&gt;blocked by CORS policy&lt;/em&gt;. The confusing part is that the request usually &lt;em&gt;did&lt;/em&gt; reach your server — often it even succeeded. CORS doesn't stop the request; it stops your JavaScript from &lt;strong&gt;reading the response&lt;/strong&gt;. That's also why the same call works fine in curl or Postman: CORS is enforced by browsers, and only browsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What counts as cross-origin
&lt;/h2&gt;

&lt;p&gt;An origin is the scheme, host and port together. These are all different origins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;https://app.example.com&lt;/code&gt; and &lt;code&gt;https://api.example.com&lt;/code&gt; — different host&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;http://localhost:3000&lt;/code&gt; and &lt;code&gt;http://localhost:8080&lt;/code&gt; — different port&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;http://example.com&lt;/code&gt; and &lt;code&gt;https://example.com&lt;/code&gt; — different scheme&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a page on one origin fetches from another, the browser asks the server for permission through response headers, chiefly &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simple requests and preflights
&lt;/h2&gt;

&lt;p&gt;Some requests are sent straight away, and the browser checks the response headers afterwards. Others trigger a &lt;strong&gt;preflight&lt;/strong&gt;: an automatic &lt;code&gt;OPTIONS&lt;/code&gt; request that asks whether the real request is allowed. A preflight happens when, among other things, the request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;uses a method other than GET, HEAD or POST,&lt;/li&gt;
&lt;li&gt;sends a header outside a short safe list — &lt;code&gt;Authorization&lt;/code&gt; is the classic one, or&lt;/li&gt;
&lt;li&gt;sends &lt;code&gt;Content-Type: application/json&lt;/code&gt; (only &lt;code&gt;text/plain&lt;/code&gt;, &lt;code&gt;multipart/form-data&lt;/code&gt; and &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt; are exempt).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So almost every real API call — JSON body, bearer token — is preflighted. The server must answer that &lt;code&gt;OPTIONS&lt;/code&gt; request successfully, with the right headers, &lt;em&gt;before&lt;/em&gt; the browser sends the real one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The common errors, and their fixes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"No 'Access-Control-Allow-Origin' header is present on the requested resource."&lt;/strong&gt;&lt;br&gt;
The response didn't include the header. Add it, with the calling origin as the value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Vary: Origin&lt;/code&gt; matters when you choose the value per request: it stops a CDN from serving one origin's response to another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Response to preflight request doesn't pass access control check: It does not have HTTP ok status."&lt;/strong&gt;&lt;br&gt;
Your server answered the &lt;code&gt;OPTIONS&lt;/code&gt; preflight with an error — often &lt;a href="https://www.devkult.com/http/401" rel="noopener noreferrer"&gt;401&lt;/a&gt; because auth middleware ran first, or &lt;a href="https://www.devkult.com/http/404" rel="noopener noreferrer"&gt;404&lt;/a&gt;/&lt;a href="https://www.devkult.com/http/405" rel="noopener noreferrer"&gt;405&lt;/a&gt; because no route handles OPTIONS. Answer preflights before authentication, with &lt;a href="https://www.devkult.com/http/204" rel="noopener noreferrer"&gt;204 No Content&lt;/a&gt; and the CORS headers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response."&lt;/strong&gt;&lt;br&gt;
List every custom header the client sends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;"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'."&lt;/strong&gt;&lt;br&gt;
Requests that send cookies (&lt;code&gt;credentials: "include"&lt;/code&gt;) can't use &lt;code&gt;*&lt;/code&gt;. Return the specific origin plus &lt;code&gt;Access-Control-Allow-Credentials: true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Redirect is not allowed for a preflight request."&lt;/strong&gt;&lt;br&gt;
The preflight hit a redirect — commonly a trailing-slash or http→https rule. Call the final URL directly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Server examples
&lt;/h2&gt;

&lt;p&gt;Express, with the &lt;code&gt;cors&lt;/code&gt; package and an explicit allow-list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;cors&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://app.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:5173&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// cache the preflight for 10 minutes&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Next.js route handler answers preflights by exporting &lt;code&gt;OPTIONS&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://app.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Methods&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET, POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Headers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization, Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;OPTIONS&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CORS&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CORS&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;nginx in front of an API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request_method&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;OPTIONS)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt; &lt;span class="s"&gt;"https://app.example.com"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Allow-Methods&lt;/span&gt; &lt;span class="s"&gt;"GET,&lt;/span&gt; &lt;span class="s"&gt;POST,&lt;/span&gt; &lt;span class="s"&gt;PUT,&lt;/span&gt; &lt;span class="s"&gt;DELETE"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Allow-Headers&lt;/span&gt; &lt;span class="s"&gt;"Authorization,&lt;/span&gt; &lt;span class="s"&gt;Content-Type"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Max-Age&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt; &lt;span class="s"&gt;"https://app.example.com"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;always&lt;/code&gt; flag makes nginx add the header to error responses too — without it, a 500 from your API shows up in the browser as a CORS error, which hides the real problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't reflect any origin with credentials.&lt;/strong&gt; Echoing whatever &lt;code&gt;Origin&lt;/code&gt; arrives, together with &lt;code&gt;Access-Control-Allow-Credentials: true&lt;/code&gt;, lets &lt;em&gt;any&lt;/em&gt; website read your users' data with their cookies. Check origins against an allow-list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't use &lt;code&gt;mode: "no-cors"&lt;/code&gt;.&lt;/strong&gt; It makes the error disappear by giving you an opaque response you can't read. The request "works"; your code gets nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't fight CORS in development.&lt;/strong&gt; Use your dev server's proxy (Vite's &lt;code&gt;server.proxy&lt;/code&gt;, Next.js &lt;code&gt;rewrites&lt;/code&gt;) so the browser sees one origin.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing a preflight with curl
&lt;/h2&gt;

&lt;p&gt;curl doesn't enforce CORS, but it can show you exactly what the browser would see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; OPTIONS https://api.example.com/items &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Origin: https://app.example.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Access-Control-Request-Method: POST"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Access-Control-Request-Headers: authorization, content-type"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for a 2xx status and the three &lt;code&gt;Access-Control-Allow-*&lt;/code&gt; headers. To check what a list of endpoints returns, paste them into the &lt;a href="https://www.devkult.com/tools/web/http-status-checker" rel="noopener noreferrer"&gt;HTTP Status Checker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One last tip: a preflight is cached for as long as &lt;code&gt;Access-Control-Max-Age&lt;/code&gt; says — up to 2 hours in Chrome and 24 hours in Firefox — so after changing CORS headers, test in a fresh private window.&lt;/p&gt;

&lt;p&gt;For each header in detail, see the reference pages for &lt;a href="https://www.devkult.com/headers/access-control-allow-origin" rel="noopener noreferrer"&gt;Access-Control-Allow-Origin&lt;/a&gt;, &lt;a href="https://www.devkult.com/headers/access-control-allow-methods" rel="noopener noreferrer"&gt;Access-Control-Allow-Methods&lt;/a&gt;, &lt;a href="https://www.devkult.com/headers/access-control-allow-headers" rel="noopener noreferrer"&gt;Access-Control-Allow-Headers&lt;/a&gt;, &lt;a href="https://www.devkult.com/headers/access-control-allow-credentials" rel="noopener noreferrer"&gt;Access-Control-Allow-Credentials&lt;/a&gt; and &lt;a href="https://www.devkult.com/headers/origin" rel="noopener noreferrer"&gt;Origin&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.devkult.com/blog/cors-errors-explained" rel="noopener noreferrer"&gt;devkult.com&lt;/a&gt;. For every CORS header in detail, see the &lt;a href="https://www.devkult.com/headers" rel="noopener noreferrer"&gt;HTTP headers reference&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>api</category>
      <category>security</category>
    </item>
    <item>
      <title>ERR_TOO_MANY_REDIRECTS: How to Find and Fix a Redirect Loop</title>
      <dc:creator>head_of_david</dc:creator>
      <pubDate>Tue, 20 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/head_of_david/errtoomanyredirects-how-to-find-and-fix-a-redirect-loop-156h</link>
      <guid>https://dev.to/head_of_david/errtoomanyredirects-how-to-find-and-fix-a-redirect-loop-156h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This page isn't working. example.com redirected you too many times. ERR_TOO_MANY_REDIRECTS&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Firefox calls it "The page isn't redirecting properly". Both mean the same thing: the browser followed redirects until it hit its limit (20 in Chrome and Firefox), because two or more rules keep sending it back and forth. Something says "go to B", and B says "go to A".&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: trace the loop
&lt;/h2&gt;

&lt;p&gt;curl shows each hop without any browser cache in the way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sIL&lt;/span&gt; &lt;span class="nt"&gt;--max-redirs&lt;/span&gt; 10 http://example.com/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"^(HTTP|location)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTTP/1.1 301 Moved Permanently
location: https://example.com/
HTTP/2 301
location: http://example.com/
HTTP/1.1 301 Moved Permanently
location: https://example.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can see the circle — here, HTTP → HTTPS → HTTP. The pattern in the &lt;code&gt;location&lt;/code&gt; lines tells you which kind of loop you have. The &lt;a href="https://www.devkult.com/tools/web/http-status-checker" rel="noopener noreferrer"&gt;HTTP Status Checker&lt;/a&gt; shows the same chain in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual causes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  HTTPS redirect behind a proxy or CDN
&lt;/h3&gt;

&lt;p&gt;The most common loop. Your app redirects HTTP to HTTPS, but it sits behind a load balancer or CDN that talks to it over plain HTTP. Every request arrives as HTTP, so the app redirects every request — forever.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare&lt;/strong&gt;: with SSL mode &lt;strong&gt;Flexible&lt;/strong&gt;, Cloudflare connects to your server over HTTP. If the server redirects to HTTPS, you loop. Switch to &lt;strong&gt;Full (strict)&lt;/strong&gt; and install a certificate on the origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your app behind a proxy&lt;/strong&gt;: tell the framework to trust the proxy's &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; header, so it knows the original request was HTTPS. In Express, &lt;code&gt;app.set("trust proxy", 1)&lt;/code&gt;; in Django, &lt;code&gt;SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")&lt;/code&gt;. See &lt;a href="https://www.devkult.com/headers/x-forwarded-for" rel="noopener noreferrer"&gt;X-Forwarded-For&lt;/a&gt; for how the forwarded headers work.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  www and apex rules fighting
&lt;/h3&gt;

&lt;p&gt;One layer redirects &lt;code&gt;www.example.com&lt;/code&gt; → &lt;code&gt;example.com&lt;/code&gt; while another redirects &lt;code&gt;example.com&lt;/code&gt; → &lt;code&gt;www.example.com&lt;/code&gt; — often the hosting dashboard against an nginx or app rule. Pick one canonical host and make every layer agree.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trailing-slash rules
&lt;/h3&gt;

&lt;p&gt;A framework setting adds a trailing slash (&lt;code&gt;/docs&lt;/code&gt; → &lt;code&gt;/docs/&lt;/code&gt;) while the server or CDN removes it. Same fix: decide on one form and remove the other rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Login loops
&lt;/h3&gt;

&lt;p&gt;A page requires authentication and redirects to &lt;code&gt;/login&lt;/code&gt;; &lt;code&gt;/login&lt;/code&gt; thinks you're logged in (or itself requires auth) and redirects back. Two common roots:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The session cookie is never stored — for example it's marked &lt;code&gt;Secure&lt;/code&gt; but the site runs on plain HTTP, or its &lt;code&gt;Domain&lt;/code&gt; doesn't match. See &lt;a href="https://www.devkult.com/headers/set-cookie" rel="noopener noreferrer"&gt;Set-Cookie&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The cookie isn't sent on the request that matters — a cross-site POST back from an identity provider, blocked by &lt;code&gt;SameSite&lt;/code&gt;. See &lt;a href="https://www.devkult.com/blog/samesite-cookies-explained" rel="noopener noreferrer"&gt;SameSite cookies explained&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CMS address settings
&lt;/h3&gt;

&lt;p&gt;In WordPress, a mismatch between the "WordPress Address" and "Site Address" settings — &lt;code&gt;http://&lt;/code&gt; against &lt;code&gt;https://&lt;/code&gt;, or &lt;code&gt;www&lt;/code&gt; against no &lt;code&gt;www&lt;/code&gt; — loops the same way. Make both identical and match your real canonical URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: rule out the browser's cache
&lt;/h2&gt;

&lt;p&gt;A permanent redirect (&lt;a href="https://www.devkult.com/http/301" rel="noopener noreferrer"&gt;301&lt;/a&gt; or &lt;a href="https://www.devkult.com/http/308" rel="noopener noreferrer"&gt;308&lt;/a&gt;) can be cached by the browser indefinitely. After you fix the server, the browser may keep looping on its cached redirect. Test in a private window or with curl, and clear site data (DevTools → Application → Storage → Clear site data) for the real browser.&lt;/p&gt;

&lt;p&gt;This is also why it's worth testing new redirects as &lt;a href="https://www.devkult.com/http/302" rel="noopener noreferrer"&gt;302&lt;/a&gt; first — see &lt;a href="https://www.devkult.com/blog/301-vs-302-vs-307-vs-308" rel="noopener noreferrer"&gt;301 vs 302 vs 307 vs 308&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Trace the hops with &lt;code&gt;curl -sIL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Find the two rules that disagree — protocol, host, slash or auth.&lt;/li&gt;
&lt;li&gt;Keep one rule; remove or align the other.&lt;/li&gt;
&lt;li&gt;Retest in a private window.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.devkult.com/blog/err-too-many-redirects" rel="noopener noreferrer"&gt;devkult.com&lt;/a&gt;. To trace redirect chains for a list of URLs at once, try the free &lt;a href="https://www.devkult.com/tools/web/http-status-checker" rel="noopener noreferrer"&gt;HTTP Status Checker&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>http</category>
      <category>nginx</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Unexpected Token in JSON at Position 0: What It Means and How to Fix It</title>
      <dc:creator>head_of_david</dc:creator>
      <pubDate>Fri, 16 Oct 2026 11:07:32 +0000</pubDate>
      <link>https://dev.to/head_of_david/unexpected-token-in-json-at-position-0-what-it-means-and-how-to-fix-it-2efo</link>
      <guid>https://dev.to/head_of_david/unexpected-token-in-json-at-position-0-what-it-means-and-how-to-fix-it-2efo</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SyntaxError: Unexpected token '&amp;lt;', "&amp;lt;!DOCTYPE "... is not valid JSON
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Older browsers and Node versions word it as &lt;code&gt;Unexpected token &amp;lt; in JSON at position 0&lt;/code&gt;. Either way, the message is telling you something specific: the first character of what you tried to parse as JSON was &lt;code&gt;&amp;lt;&lt;/code&gt; — and JSON never starts with &lt;code&gt;&amp;lt;&lt;/code&gt;. &lt;strong&gt;You received HTML, not JSON.&lt;/strong&gt; The parser is fine; the response isn't what you think it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The messages, decoded
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Message&lt;/th&gt;
&lt;th&gt;What you actually received&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Unexpected token '&amp;lt;', "&amp;lt;!DOCTYPE "...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An HTML page — usually an error page, a login page or your app's &lt;code&gt;index.html&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Unexpected end of JSON input&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An empty body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"[object Object]" is not valid JSON&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;An object you already parsed, passed to &lt;code&gt;JSON.parse&lt;/code&gt; again&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"undefined" is not valid JSON&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The string &lt;code&gt;"undefined"&lt;/code&gt; — often from &lt;code&gt;localStorage&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Unexpected token '﻿'&lt;/code&gt; (an invisible character)&lt;/td&gt;
&lt;td&gt;A UTF-8 byte order mark before the JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Expected double-quoted property name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A trailing comma, or single-quoted keys&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Firefox says &lt;code&gt;JSON.parse: unexpected character at line 1 column 1 of the JSON data&lt;/code&gt;, and Safari says &lt;code&gt;JSON Parse error: Unrecognized token '&amp;lt;'&lt;/code&gt;. Python's equivalent is &lt;code&gt;JSONDecodeError: Expecting value: line 1 column 1 (char 0)&lt;/code&gt;, which &lt;code&gt;requests&lt;/code&gt; raises from &lt;code&gt;.json()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: look at what you got
&lt;/h2&gt;

&lt;p&gt;Read the body as text before parsing it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/items&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or open DevTools → Network, click the request and look at the &lt;strong&gt;Response&lt;/strong&gt; tab. Nine times out of ten, the answer is right there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: match it to a cause
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;An HTML error page.&lt;/strong&gt; The request hit a &lt;a href="https://www.devkult.com/http/404" rel="noopener noreferrer"&gt;404&lt;/a&gt; or &lt;a href="https://www.devkult.com/http/500" rel="noopener noreferrer"&gt;500&lt;/a&gt;, and the server returned its HTML error page. Check the status code — &lt;code&gt;response.json()&lt;/code&gt; doesn't care whether the request succeeded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your app's own &lt;code&gt;index.html&lt;/code&gt;.&lt;/strong&gt; Single-page apps return &lt;code&gt;index.html&lt;/code&gt; for every unknown path, so a wrong API URL (&lt;code&gt;/api/item&lt;/code&gt; instead of &lt;code&gt;/api/items&lt;/code&gt;, or a missing &lt;code&gt;/api&lt;/code&gt; prefix in production) gets HTML with status 200. Also check your dev server proxy: in development &lt;code&gt;/api&lt;/code&gt; may be forwarded to the backend, but in production it isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A login page.&lt;/strong&gt; An expired session redirects to &lt;code&gt;/login&lt;/code&gt;; &lt;code&gt;fetch&lt;/code&gt; follows the redirect and hands you the login HTML. &lt;code&gt;res.redirected&lt;/code&gt; will be &lt;code&gt;true&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An empty body.&lt;/strong&gt; &lt;a href="https://www.devkult.com/http/204" rel="noopener noreferrer"&gt;204 No Content&lt;/a&gt; responses and some error responses have no body, so there's nothing to parse — "Unexpected end of JSON input". Check the status before calling &lt;code&gt;.json()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parsing twice.&lt;/strong&gt; &lt;code&gt;response.json()&lt;/code&gt; and axios already return objects. Calling &lt;code&gt;JSON.parse()&lt;/code&gt; on the result turns it into the string &lt;code&gt;"[object Object]"&lt;/code&gt; first, which fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not quite JSON.&lt;/strong&gt; Hand-written JSON with trailing commas, comments, single quotes or &lt;code&gt;NaN&lt;/code&gt; isn't valid. Paste it into the &lt;a href="https://www.devkult.com/tools/json/json-validator" rel="noopener noreferrer"&gt;JSON validator&lt;/a&gt; to find the exact position.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: guard your fetch calls
&lt;/h2&gt;

&lt;p&gt;Check the status and the &lt;a href="https://www.devkult.com/headers/content-type" rel="noopener noreferrer"&gt;Content-Type&lt;/a&gt; before parsing, and include the start of the body in the error so the next failure explains itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; from &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no content type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;): &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the server side, make your API return JSON errors with &lt;code&gt;Content-Type: application/json&lt;/code&gt; — including for 404s and 500s — so clients get a parseable message instead of an HTML page. The correct type for JSON is &lt;a href="https://www.devkult.com/mime/json" rel="noopener noreferrer"&gt;&lt;code&gt;application/json&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The localStorage variant
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;settings&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// fine when the key exists&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the key doesn't exist, &lt;code&gt;getItem&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt;, and &lt;code&gt;JSON.parse(null)&lt;/code&gt; happens to return &lt;code&gt;null&lt;/code&gt; — no error. But if some code once saved &lt;code&gt;localStorage.setItem("settings", undefined)&lt;/code&gt;, the stored value is the string &lt;code&gt;"undefined"&lt;/code&gt;, which throws. Guard and reset:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readSettings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;settings&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;settings&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To inspect or tidy a JSON payload by hand, use the &lt;a href="https://www.devkult.com/tools/json/json-formatter" rel="noopener noreferrer"&gt;JSON formatter&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.devkult.com/blog/unexpected-token-in-json" rel="noopener noreferrer"&gt;devkult.com&lt;/a&gt;. When you need to find exactly where a payload breaks, paste it into the free &lt;a href="https://www.devkult.com/tools/json/json-validator" rel="noopener noreferrer"&gt;JSON validator&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>json</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
