javascripthttpcorsfetchdeveloper-console

cross-domain fetch() throws "Failed to fetch" from page but not from the dev console


Related to Getting "TypeError: failed to fetch" when the request hasn't actually failed, but that didn't seem to work in the dev console either.

I'm accessing a restconf server directly from a web page. Static pages are served from another server.

I'm trying to call an RPC I've made, though I doubt that specific detail matters - it's just a POST request with an Authorization header.

The problem I'm facing is that it works from the dev console but not from the page code.

Calling this from the dev console works fine:

fetch('http://172.17.0.2:8008/restconf/operations/login', {
   method: 'POST', 
   headers: {
     accept: 'application/yang-data+json', 
     authorization: 'Basic '+btoa('admin:admin')
   }
})
.then(resp => resp.json())
.then(json => console.log('session token:', json['output']['session-token']))
.then(err => console.error(err));

It outputs:

session token: e6cfd60b-f96e-5d8b-b10c-16a5d71a6367

But doing the exact same call (verbatim copy of the above) from within the code on the page, it fails.

Do note that the http request actually succeeds (dev console says "status 200 OK"), but fetch() still throws an exception (the so-very-endearingly-specific TypeError: Failed to fetch).

I can see the requests & responses using ngrep in a terminal (for some reason the dev console won't display the body):

Here are the request and response:

POST /restconf/operations/login HTTP/1.1
Host: 172.17.0.2:8008
Connection: keep-alive
Content-Length: 0
Pragma: no-cache
Cache-Control: no-cache
accept: application/yang-data+json
Origin: http://localhost:5000
authorization: Basic YWRtaW46YWRtaW4=
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36
Referer: http://localhost:5000/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
HTTP/1.1 200 OK
Date: Wed, 22 Jan 2020 11:09:17 GMT
Cache-Control: private, no-cache, must-revalidate, proxy-revalidate
Content-Length: 138
Content-Type: application/yang-data+json
Pragma: no-cache
Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Auth-Token
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
Access-Control-Allow-Origin: http://localhost:5000
Content-Security-Policy: default-src 'self'; block-all-mixed-content; base-uri 'self'; frame-ancestors 'none';
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

{
  "output": {
    "session-token": "e6cfd60b-f96e-5d8b-b10c-16a5d71a6367",
    "expires": "2020-01-22T15:09:17.491906"
  }
}

(The OPTIONS request and response omitted for brevity. It's not terribly exciting, and from my understanding the POST is never sent if the OPTIONS does not pass validation)

Both request and response in the two cases are identical apart from the time stamps. So I'm assuming there's some validation performed by the browser in the page case that doesn't happen from the dev console. But what?

I read somewhere that the allow-origin header isn't allowed to be an asterisk in some cases, which I was using before. Changing it to the actual URL didn't change anything.

Ideas greatly appreciated!


Solution

  • This is now "solved".

    Turns out the informative Failed to fetch, was really trying to say Request aborted by leaving page. If it just would've said that, plenty of head-scratching could've been avoided.

    So yes, the issue was that the fetch() that's at the center of this Q was being called in response to clicking a submit button in a form which had been incorrectly set up to not reload the page upon submission. Thus, when the page was reloaded the pending requests were aborted, throwing Failed to fetch.

    I'm using Svelte, which I'm not entirely familiar with, and disabling the page reload on submission is kind of non-obvious. At least if you're already familiar with the classic way of doing it (i.e. return false).

    In addition to this mistake, I also fooled myself by having "Preserve log" enabled in the dev console, and thinking that the message "Navigated to ..." it outputs on page reload was because of my code later in the chain (which I was "getting to later"...).