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.

Replace `this.fetch` with `this.get`

See original GitHub issue

Stumbled into a bit of a rabbit hole while adding prerendering to adapter-node — because this.fetch is expected to work exactly the same as fetch, we have to account for the possibility that someone will this.fetch('/some-static-file.json), which means that the prerenderer needs to know where the static files could live, which muddies the API somewhat.

It’s also just a bit of an awkward API:

  • Sapper doesn’t currently handle URLs relative to the requested page (though this could easily change, and has in fact changed in the current app-utils)
  • The whole res = await this.fetch(...); data = await res.json() dance is a bit of a pain, honestly
  • It’s easy to forget that you need credentials: 'include' if you’re using session
  • fetch supports non-GET methods, which don’t make sense in the context of preload (or load, as the case may be)

I’d like to replace it with this.get:

<script context="module">
  export async function load({ params }) {
    return await this.get(`./${params.slug}.json`);
  }
</script>
  • Only works with server routes, not static files or external resources. Simpler
  • Accepts a URL (easiest way to identify server routes, which will of course continue to work with plain fetch), which can be relative to the requested page
  • Always includes credentials
  • Returns a promise for the body of the response. If the Content-Type is application/json, is JSON.parsed. If it’s text/*, is returned as text. Otherwise is returned as an array buffer
  • Non-200 responses throw an error (augmented with status code), which is equivalent to calling this.error(status, error), except that you can catch and handle if necessary. For common cases, this eliminates the need to add error handling around the response returned from this.fetch

We could also expose get alongside the other app functions (goto et al) for use outside preload/load functions. (It would probably just throw in Node.) That’s not necessary for this.get to have value though.

Of course there are still times when it’s useful to be able to use fetch in preload/load — hn.svelte.dev fetches data from https://api.hnpwa.com, for example, and it’s good to be able to do this isomorphically (rather than forcing the app to always connect to the app’s own server, like ahem some frameworks). But if it’s only to be used for external URLs, then there’s no need to use this.fetch, we can just use regular fetch. We can polyfill it in Node with @rollup/plugin-inject and node-fetch (which already powers this.fetch). With this approach, many apps wouldn’t need node-fetch at all, whereas currently it has to be included because of this.fetch.

This seems like all upside to me, except for the breakingness of the change. But if there was ever a time for breaking changes this is it. Thoughts?

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:38 (38 by maintainers)

github_iconTop GitHub Comments

1reaction
Conduitrycommented, Dec 4, 2020

I was imagining that this would work somewhat similar to an exported site, in that the server would run the preload function, and track which URLs were retrieved, and note the responses somewhere. The server will have already sent the appropriate headers when it was requesting the endpoints that it writes out the responses to in the rendered HTML. The headers thing only seems to be a problem if the values of the headers are non-deterministic in some way (or vary depending on process.browser) (which would be a problem anyway with Sapper’s existing preload handling), or if preload hit the same endpoint twice with different HTTP headers (which seems really unusual).

I’m not sure what the point about generated code is in relation to. I’m not seeing any generated code in Rich’s suggestion. The client-side this.get/this.fetch/whatever would simply first look whether the URL it’s requesting was one of the ones that had a response baked into the HTML that was rendered for the page.

1reaction
Rich-Harriscommented, Feb 4, 2022

This is just a fleeting thought that I’d like to write down before it evaporates. I haven’t thought it through:

Sometimes, I want to do some sort of processing of my data inside preload. For example, I might have this…

export async function preload() {
  const json = url => this.fetch(url).then(r => r.json());
  const [states, counties] = Promise.all([json('/states.json'), json('/counties.json')]);

  const state_lookup = new Map();
  states.forEach(state => {
    state_lookup.set(state.fips, state);
    state.counties = counties.filter(county => county.fips.startsWith(state.fips));
  });

  counties.forEach(county => county.state = state_lookup.get(county.fips.slice(0, 2));

  return { states, counties };
}

…which links objects representing states to objects representing counties. Thanks to devalue, that’s possible, because we can serialize the preloaded data even though it contains cyclical and repeated references. But devalue can’t serialize everything — it couldn’t handle counties: counties.map(d => new County(d)), for example.

Another example, which I’m currently facing. There’s a huge difference between these:

<script context="module">
  import decompress from './_utils.js';

  export async function preload() {
    const data = await this.fetch('/compressed-data.json').then(r => r.json());
    return {
      data: decompress(data)
    };
  }
</script>

<script>
  export let data;
</script>
<script context="module">
  export async function preload() {
    const data = await this.fetch('/compressed-data.json').then(r => r.json());
    return {
      compressed_data: data
    };
  }
</script>

<script>
  import decompress from './_utils.js';
  
  export let compressed_data;
  $: data = decompress(compressed_data);
</script>

Conceptually, the first one makes more sense: the component doesn’t care about the data in its compressed state; preload is the appropriate place to do that work (in the same way that the component doesn’t care about the JSON, it only cares about the deserialized result. Decompression and deserialization fall into the same category, I think). But in practical terms, the second one is much better, because otherwise we have to serialize the decompressed data for the server-rendered page, which defeats the object of compression.

Those of us reading this issue understand the mechanics well enough to know to prefer the second form so that we don’t serialize and transmit the decompressed data. Many people wouldn’t.

So I wonder if we should change our approach: what if we didn’t serialize the output of preload, but instead serialized the inputs, and ran preload in the client for the initial hydration, as we do for other pages? In that case, calling something like

const data = this.get('/some-data.json');

might cause the following to be stitched into the SSR’d page…

<script type="application/json" href="/some-data.json">{"answer":42}</script>

…which would be used to satisfy the this.get('/some-data.json') call in the client for the initial hydration, ensuring a) we don’t need an additional network request for that data (same as current serialization approach), and b) guaranteeing consistency between server and client renders.

Additionally, using JSON.parse is somewhat faster than an object literal or a devalue IIFE.

The ability to return functions and non-POJOs from preload would be an additional bonus.

Read more comments on GitHub >

github_iconTop Results From Across the Web

fetch() - Web APIs | MDN
Note that a request using the GET or HEAD method cannot have a body. ... Fetch with the keepalive flag is a replacement...
Read more >
How to handle this fetch() TypeError? - Stack Overflow
I get no errors when an account exists in the HIBP db, but I get a 'Request failed: TypeError: response.json is not a...
Read more >
How To Use the JavaScript Fetch API to Get Data - DigitalOcean
Fetch defaults to GET requests, but you can use all other types of requests, change the headers, and send data. Let's create a...
Read more >
How to Use Fetch with async/await - Dmitri Pavlutin
The Fetch API accesses resources across the network. You can make HTTP requests (using GET , POST and other methods), download, and upload...
Read more >
Introduction to fetch() - web.dev
The fetch() API is landing in the window object and is looking to replace XHRs.
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 Hashnode Post

No results found