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 fallthrough routes with param validators

See original GitHub issue

Describe the problem

SvelteKit has an unusual feature called fallthrough routes which allows ambiguity between routes to be resolved by the routes themselves.

It’s a powerful feature, but a niche one, and it comes at a significant cost. It adds non-trivial complexity to the SvelteKit codebase, makes things conceptually less clear, leads to undefined behaviour (should event.locals persist between different routes that are candidates for the same request?), has undesirable API implications (load has to return something, even if it doesn’t return anything), and terrible performance implications in the case where a route does fall through.

We could get 90% of the value of fallthrough routes with a fraction of the cost if we had some way of validating route parameters before attempting to render or navigate.

Describe the proposed solution

Credit for this idea goes to @mrkishi; any flaws were introduced by me.

Suppose you’re building a calendar app, and for some reason you’re forced to have routes like this:

src/routes/[date]
src/routes/[eventid]

When someone visits /2022-03-12, either route could match. Rather than adding fallthrough logic to src/routes/[date], we could add a validator that SvelteKit uses to check the format:

// src/params/date.js
export default param => /^\d{4}-\d{2}-\d{2}$/.test(param);

Seeing that 2022-03-12 passes the test, we can select the src/routes/[date] route immediately.

If we know that every eventid is a UUID, we could test that too. Since UUIDs are a generic concept, we need a more generic name than eventid:

src/routes/[eventid=uuid]

Now, we can add a UUID validator…

// src/params/uuid.js
export default param => /[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}/.test(param);

…and if someone visits /neither-a-date-nor-a-uuid we can 404 without doing anything else. Not only that, but the routes can have complete confidence that their parameters are valid.

(We could also do enum checks for things like src/routes/[year]/[month]/[day]

// src/params/month.js
const months = new Set('jan feb mar apr may jun jul aug sep oct nov dec'.split(' '));
export default param => months.has(param);

…though I don’t have any immediate thoughts on how that would internationalize.)

Validating vs parsing

One thing I’m not sure of: whether these functions should return booleans, or a parsed representation of the parameter. For example, date.js could return a Date object (or a Temporal, once that’s possible):

// src/params/date.js
export default (param) => {
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(param);
  if (!match) return;
  
  return new Date(+match[1], +match[2] - 1, +match[3]);
};

In that case, params.date would be the returned Date. This makes sense insofar as you’re not applying the regex twice, and if you change the data format from yyyy-mm-dd to something else you only need to do it in one place. But it makes the types harder to reason about, and we then have to figure out what to return when a parameter is invalid.

Alternatives considered

Server-side async validators

We could make validators even more powerful by specifying that some can only run on the server (i.e. the client communicates via JSON), and allowing them to be asynchronous. For example, if you had src/routes/blog/[post]

// src/params/post.server.js
import db from '$lib/db';

export default async (param) => {
  const post = await db.get('post', param);
  return post;
};

params.post could be a fully hydrated post object, or the route could 404 if the database didn’t contain the post in question.

This is a seductive idea (I was seduced by it) but I think it would be a mistake. It gives you a third way to load data (alongside page/section endpoints and load), leading to the question ‘where am I supposed to put this data loading logic?’, and it doesn’t scale — as soon as you need to refer to event.locals, or make a database query that involves multiple parameters, you’re back to endpoints. It’s sometimes worth sacrificing a little bit of power to make things more coherent.

Nesting validators where they’re used

In the samples above, I’ve put validators in src/params/*.js. It’s tempting to suggest colocating them with the routes that use them…

src/routes/products/__params/id.js
src/routes/products/[id].svelte
src/routes/events/__params/id.js
src/routes/events/[id].svelte

…but this feels to me like it would lead to more confusion and less code reuse. I would advocate for doing this instead…

src/routes/products/[id=productid].svelte
src/routes/events/[id=eventid].svelte

…which has the side-effect of creating a kind of central ontology of the concepts in your app.

Putting validators in a single file

My examples show src/params/date.js and src/params/uuid.js rather than this:

// src/params.js
export const date = param => /^\d{4}-\d{2}-\d{2}$/.test(param);
export const uuid = param => /[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}/.test(param);

This is intentional, so that we retain the option to code-split and lazy-load validators.

Importance

would make my life easier

Additional Information

Escape hatch

There are some legitimate (if somewhat niche) use cases for fallthrough that aren’t covered by this proposal, so it’s important to note that there is an escape hatch — you can create an umbrella route and distinguish between routes in load:

<script context="module">
  export function load(page) {
    const module = await (page.params.foo = 'one'
      ? import('./One.svelte')
      : import('./Two.svelte');

    const loaded = await module.load(page);

    return {
      ...loaded,
      props: {
        ...loaded?.props,
        component: module.default
      }
    };
  }
</script>

<script>
  export let component;
</script>

<svelte:component this={component} {...$$restProps} />

Middleware

One of the reasons I’m motivated to make this change is that people have requested some form of scoped middleware that only runs for a subset of pages:

// src/routes/admin/__middleware.js
export function handle({ event, resolve }) {
  if (!event.locals.user?.is_admin) {
    return new Response('YOU SHALL NOT PASS', {
     status: 403
    });
  }

  return resolve(event);
}

We haven’t designed it yet, but it’s likely that we’ll want to ‘chain’ handle functions — i.e. the root handle in src/hooks.js (which might become src/routes/__middleware.js) would, by calling resolve, invoke the admin middleware if someone was requesting a page under /admin.

That becomes nightmarishly hard to reason about if route matching happens during resolve rather than before handle is invoked. You can’t know, in the general case, which handle functions to execute.

Route logging

#3907 proposes a new hook for logging which route ended up handling a given request. Most likely, this would involve exposing the internal route key (i.e. /blog/my-article has a key like blog/[slug]) on the event object. If the route is known before handle runs, we don’t need to introduce another hook. This is a small example, but it shows how removing fallthrough routes would pay dividends that might not be immediately obvious.

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:8
  • Comments:15 (13 by maintainers)

github_iconTop GitHub Comments

5reactions
Rich-Harriscommented, Mar 13, 2022

In the longer term we might well want some special handling for [lang] params so that i18n is more of a first class concern, but in the meantime yes, that’s a perfect example of how this could be useful

1reaction
Rich-Harriscommented, Mar 12, 2022

I can’t remember where we were discussing this recently but we talked about handling encoding in filenames, so you could have a route like src/routes/%5f%5fhealth and it would map to __health

Read more comments on GitHub >

github_iconTop Results From Across the Web

What's new in Svelte: April 2022
The last holdout of the use-cases that required fallthrough routes, validating parameter properties, has been replaced by a more specific ...
Read more >
MVC Routing parameter optional with ActionResult required ...
I have an controller (Product) with several actions, some have a required parameter (Edit, Details, History) and some have a nullable parameter ......
Read more >
5.x API - Express.js
Routes HTTP DELETE requests to the specified path with the specified callback functions. For more information, see the routing guide. Arguments. Argument ......
Read more >
Cisco IOS IP Routing: BGP Command Reference
To remove the policy from the current template, use the no form of this ... parameters for Border Gateway Protocol (BGP) routing sessions, ......
Read more >
Route Parameter Validation Problem - Drupal Answers
My solution was to modify the javascript directing traffic to the mobile video page so that it replaced the slashes with triple underscores....
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