question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. ItΒ collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

angular.io preloading hints are misconfigured causing duplicate requests

See original GitHub issue

πŸ“š Docs or angular.io bug report

Description

See duplicate requests in the network panel + warnings in the console:

Screen Shot 2020-01-22 at 6 39 43 AM

πŸ”¬ Minimal Reproduction

visit angular.io or next.angular.io

What’s the affected URL?**

https://angular.io/…

Reproduction Steps**

Open dev console to see the warnings

Expected vs Actual Behavior**

no warnings, no duplicate requests

πŸ”₯ Exception or Error


VM17:1 A preload for 'https://next.angular.io/generated/navigation.json' is found, but is not used because the request headers do not match.
(anonymous) @ VM17:1

VM17:1 A preload for 'https://next.angular.io/generated/docs/index.json' is found, but is not used because the request headers do not match.
(anonymous) @ VM17:1

The resource https://next.angular.io/generated/navigation.json was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally.

The resource https://next.angular.io/generated/docs/index.json was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it has an appropriate `as` value and it is preloaded intentionally.

🌍 Your Environment

Browser info

Anything else relevant?

Issue Analytics

  • State:open
  • Created 4 years ago
  • Comments:6 (6 by maintainers)

github_iconTop GitHub Comments

2reactions
IgorMinarcommented, Apr 1, 2021

I talked to a few people from Chrome and this seems to be a spec bug / oversight / missing feature, so I filed an issue at https://github.com/w3c/preload/issues/157.

It will most likely take a while to get this resolved and implemented in all browsers, so we might want to consider other options including changing the default as discussed in #28915.

1reaction
gkalpakcommented, Apr 2, 2021

I looked into this and I couldn’t figure out a good way to fix it. Feels like a browser limitation (or even bug?) - see below.

NOTE: While debugging this locally, where we don’t have Firebase to set these headers, you can have the same effect by using <link> tags in index.html:

 <head>
+  <link rel="preload" href="/generated/navigation.json" as="fetch" crossorigin />
+  <link rel="preload" href="/generated/docs/index.json" as="fetch" crossorigin />

Here is my journey/findings:

  1. Since the warning mentions that the problem is the different request headers, I started by comparing the headers of the preload request with that of the actual, in-app request.

  2. The first request header that was different was Accept. It had a value of */* in the preload request and a value of application/json, text/plain, */* in the actual request. This is because the HttpClient will explicitly set Accept if not specified by the user:

    https://github.com/angular/angular/blob/4ce743dfb8b89b54093c4c9300ca24c5d1f50544/packages/common/http/src/xhr.ts#L71-L74

  3. To work around this issue, I explicitly specified Accept: */* in HttpClient#get() request here:

    -    const navigationInfo = this.http.get<NavigationResponse>(navigationPath)
    +    const navigationInfo = this.http.get<NavigationResponse>(navigationPath, {headers: {Accept: '*/*'}})
    
  4. Now the Accept headers appeared identical for the two requests in the Network tab in DevTools, but I still got the warning in the Console. So, I looked for other differences in headers.

    SpoilerThe `Accept` header is the problem. But I didn't find that out until later.
  5. The next header that was different was Sec-Fetch-Mode. It had a value of no-cors in the preload request and a value of cors in the actual request. Adding crossorigin to the preload request (as suggested anyway in some resources, including MDN) fixes the Sec-Fetch-Mode discrepancy, but…

  6. Adding crossorigin to the preload request causes it to have an Origin header, which is missing from the actual, in-app request. At this point, I thought this difference was causing the problem and I couldn’t figure out a way to force the actual request to have that header too.

  7. At this point, I tried replacing HttpClient#get() with a native fetch() or XMLHttpRequest and sure enough the warning went away (i.e. the preloaded navigation.json file was re-used for the actual, in-app request and no new request was made).

Long story short, it turns out that explicitly setting the Accept header causes the browser to consider the request headers different and not use the preloaded file. Confusingly, this happens even if the Accept header is set to the same value that is shown in the Network tab for the preload request, namely */* πŸ€·β€β™‚οΈ

So, basically, replacing this line with any of the following methods fixes the issue:

  • Working solution 1 - Using fetch():

    -    const navigationInfo = this.http.get<NavigationResponse>(navigationPath)
    +    const navigationInfo = require('rxjs').from(fetch(navigationPath).then(res => res.json()))
    
  • Working solution 2 - Using XMLHttpRequest:

    -    const navigationInfo = this.http.get<NavigationResponse>(navigationPath)
    +    const navigationInfo = require('rxjs').from(new Promise(resolve => {
    +      const xhr = new XMLHttpRequest();
    +      xhr.open('GET', navigationPath);
    +      xhr.responseType = 'text';
    +      xhr.onload = () => resolve(JSON.parse(xhr.responseText));
    +      xhr.send(null);
    +    }))
    

    NOTE: This is essentially what HttpClient#get() does under hood with the exception that HttpClient#get() will also call xhr.setRequestHeader('Accept', 'application/json, text/plain, */*').

Similarly, the following variations of the above solutions stop to work as soon as you try to set the Accept header:

  • Non-working solution 1 - Using fetch() (with the Accept header):

    -    const navigationInfo = this.http.get<NavigationResponse>(navigationPath)
    +    const navigationInfo = require('rxjs').from(fetch(navigationPath, {headers: {Accept: '*/*'}}).then(res => res.json()))
    
  • Non-working solution 2 - Using XMLHttpRequest (with the Accept header):

    -    const navigationInfo = this.http.get<NavigationResponse>(navigationPath)
    +    const navigationInfo = require('rxjs').from(new Promise(resolve => {
    +      const xhr = new XMLHttpRequest();
    +      xhr.open('GET', navigationPath);
    +      xhr.setRequestHeader('Accept', '*/*');
    +      xhr.responseType = 'text';
    +      xhr.onload = () => resolve(JSON.parse(xhr.responseText));
    +      xhr.send(null);
    +    }))
    

    NOTE: This is essentially exactly what HttpClient#get() does under hood for this request.

BONUS

Hacky working solution tricking HttpClient into not adding the Accept header:

-import { HttpClient } from '@angular/common/http';
+import { HttpClient, HttpHeaders } from '@angular/common/http';
 ...
 export class NavigationService {
   ...
   private fetchNavigationInfo(): Observable<NavigationResponse> {
+    class HackyHttpHeaders extends HttpHeaders {
+      has(name: string): boolean {
+        // Pretend the `Accept` header is set, so `HttpClient` will not try to set the default value.
+        return (name.toLowerCase() === 'accept') ? true : super.has(name);
+      }
+    }
-    const navigationInfo = this.http.get<NavigationResponse>(navigationPath)
+    const navigationInfo = this.http.get<NavigationResponse>(navigationPath, {headers: new HackyHttpHeaders()})
Read more comments on GitHub >

github_iconTop Results From Across the Web

Duplicate request is always performed when ... - Stack Overflow
The function this.handleError() will be called only once. Which means that the duplicate request is not really taken and processed. Check theΒ ...
Read more >
Pitfalls and Common Mistakes | NGINX
The most frequent issue we see happens when someone attempts to just copy and paste a configuration snippet from some other guide. Not...
Read more >
Angular: How to prevent duplicated HTTP requests
Every time we call it, we'll make a new HTTP request to the server. A first solution is simply caching the response: 1....
Read more >
X-Content-Type-Options - HTTP - MDN Web Docs - Mozilla
Blocks a request if the request destination is of type style and the MIME type is not text/css , or of type script...
Read more >
Safari Technology Preview Release Notes - Apple Developer
Changed to not route the navigation preload response body to the service worker ... Fixed fetch event handling delays caused by other JavaScript...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found