sveltejs / kit

web development, streamlined
https://kit.svelte.dev
MIT License
18.43k stars 1.88k forks source link

`reroute` hook breaks when deployed on Vercel if the app is deployed as multiple functions #11879

Open LorisSigrist opened 6 months ago

LorisSigrist commented 6 months ago

Describe the bug

An App that's deployed to vercel using @sveltejs/adapter-vercel will break if it is both:

  1. Using the reroute hook
  2. Deployed as multiple functions, either because the project is large or it is using split: true

When hitting a route that should get rerouted, it will instead return a 404.

This has caused the localised routing provided by @inlang/paraglide-sveltekit to break when deployed to Vercel. opral/inlang-paraglide-js#32

Reproduction

Repo: https://github.com/LorisSigrist/sveltekit-adapter-vercel-rerotute-bug-reproduction Deployment: https://sveltekit-adapter-vercel-rerotute-bug-reproduction.vercel.app/

The route /some-page should get rerouted to /, but isn't in production. Instead a 404 is returned

Logs

No response

System Info

System:
    OS: macOS 14.3.1
    CPU: (14) arm64 Apple M3 Max
    Memory: 2.08 GB / 36.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 18.19.1 - /opt/homebrew/bin/node
    npm: 10.2.4 - /opt/homebrew/bin/npm
    pnpm: 8.15.3 - /opt/homebrew/bin/pnpm
  Browsers:
    Safari: 17.3.1
  npmPackages:
    @sveltejs/adapter-vercel: ^5.1.0 => 5.1.0 
    @sveltejs/kit: ^2.0.0 => 2.5.0 
    @sveltejs/vite-plugin-svelte: ^3.0.0 => 3.0.2 
    svelte: ^4.2.7 => 4.2.11 
    vite: ^5.0.3 => 5.1.4

Severity

blocking all usage of SvelteKit (on Vercel)

Additional Information

No response

LorisSigrist commented 6 months ago

What would probably be required here is to run the reroute hook in a Vercel Middleware function.

This isn't hard, per-se, but it would likely require bundling hook.js separately & extending the adapter API in order to get access to it.

benjaminwaterlot commented 6 months ago

Not sure if it might be related or not but I encounter another issue that happens in the exact same conditions. Sadly I've been unable to find the time to create and deploy on Vercel a reproduction. Sorry if this is useless noise 🙏

Issue:

In a form action, I trigger an internal fetch call to a +server.ts route handler. This route handler returns json data (return json({ ... })). The route handler crashes with TypeError: immutable (full error below), and the action receives { message: 'Internal Error' }. Despite what the error message implies, I'm not trying to either set cookies or headers anywhere.

Conditions:

Error details

TypeError: immutable
    at _Headers.append (node:internal/deps/undici/undici:2105:17)
    at add_cookies_to_headers (file:///var/task/vercel/path0/apps/client/.svelte-kit/output/server/index.js:2320:13)
    at file:///var/task/vercel/path0/apps/client/.svelte-kit/output/server/index.js:2649:9
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async helpersHandle (file:///var/task/vercel/path0/apps/client/.svelte-kit/output/server/chunks/hooks.server.js:1181:18)
    at async respond (file:///var/task/vercel/path0/apps/client/.svelte-kit/output/server/index.js:2638:22)
    at async fetch (file:///var/task/vercel/path0/apps/client/.svelte-kit/output/server/index.js:2388:26)

Environment

    "@sveltejs/adapter-vercel": "3.1.0",
    "@sveltejs/kit": "1.30.4",
    "svelte": "4.2.12",

And Node.js 20.x on Vercel.

dkmooers commented 4 months ago

Same issue here - reroute works locally, but breaks in prod on Vercel.

Anyone developed a fix or workaround for this?

Our alternative is to use a NextJS micro-app for handling the transparent rerouting.

jerriclynsjohn commented 4 months ago

Oh man I'm really looking forward to a fix.

dkmooers commented 3 months ago

Has anyone investigated what @LorisSigrist's idea of implementing reroute in Vercel Middleware would involve?

With our hack NextJS reroute() mock micro-app, I have to use data-sveltekit-reload on every internal SvelteKit link for routes that are transparently proxied/rewritten, which forces a full-page refresh.

dkmooers commented 3 months ago

Now I'm seeing where the Vercel docs say you can't use Edge Middleware with SvelteKit for doing URL rewrites:

https://vercel.com/docs/frameworks/sveltekit#edge-middleware

Edge Middleware is useful for modifying responses before they're sent to a user. We recommend using SvelteKit's server hooks to modify responses. Due to SvelteKit's client-side rendering, you cannot use Vercel's Edge Middleware with SvelteKit.

So, hm, anyone have any other ideas? Is there a way to get reroute() to work on Vercel?

dkmooers commented 3 months ago

In lieu of this, I'm considering going to a monorepo setup, and splitting off one major route category into another SvelteKit app, as a more robust workaround than the NextJS middleware setup we have currently, which will allow us to preserve snappy SvelteKit routing, preloads and page transitions. Might be a good solution for now!

eltigerchino commented 3 months ago

Going to assume this is an issue for the netlify adapter too (and any adapters that split up the routes into different functions).

Also, if we do use a middleware to reroute the request url before it checks the static config to run the correct vercel function, we would also need a way to preserve the original url too?

eltigerchino commented 3 months ago

Here's a POC of a Vercel edge middleware that reroutes given the following src/hooks.ts file.

// src/hooks.ts
import type { Reroute } from '@sveltejs/kit';

const translated: Record<string, string> = {
    '/en/about': '/en/about',
    '/de/ueber-uns': '/de/about',
    '/fr/a-propos': '/fr/about',
};

export const reroute: Reroute = ({ url }) => {
    if (url.pathname in translated) {
        return translated[url.pathname];
    }
};

https://11879.vercel.app/en/about is rerouted to the /about route

.vercel/output/config.json

{
    "version": 3,
    "routes": [
        {
            "src": "/_app/immutable/.+",
            "headers": {
                "cache-control": "public, immutable, max-age=31536000"
            }
        },
        {
            "handle": "filesystem"
        },
        {
      "src": "/(.*)",
      "middlewarePath": "reroute",
            "continue": true
    },
        {
            "src": "^/?(?:/__data.json)?$",
            "dest": "/fn-0"
        },
        {
            "src": "^/about/?(?:/__data.json)?$",
            "dest": "/fn-1"
        },
        {
            "src": "/.*",
            "dest": "/fn"
        }
    ],
    "overrides": {}
}

.vercel/output/functions/reroute.func/index.js

import { reroute } from './hooks.js';

// see https://vercel.com/docs/functions/edge-middleware/middleware-api#rewrites

/**
 * https://github.com/vercel/vercel/blob/4337ea0654c4ee2c91c4464540f879d43da6696f/packages/edge/src/middleware-helpers.ts#L38-L55
 * @param {*} init
 * @param {Headers} headers
 */
function handleMiddlewareField(init, headers) {
    if (init?.request?.headers) {
        if (!(init.request.headers instanceof Headers)) {
            throw new Error('request.headers must be an instance of Headers');
        }

        const keys = [];
        for (const [key, value] of init.request.headers) {
            headers.set('x-middleware-request-' + key, value);
            keys.push(key);
        }

        headers.set('x-middleware-override-headers', keys.join(','));
    }
}

/**
 * https://github.com/vercel/vercel/blob/4337ea0654c4ee2c91c4464540f879d43da6696f/packages/edge/src/middleware-helpers.ts#L101-L114
 * @param {string | URL} destination
 * @returns {Response}
 */
export function rewrite(destination) {
    const headers = new Headers({});
    headers.set('x-middleware-rewrite', String(destination));

    handleMiddlewareField(undefined, headers);

    return new Response(null, {
        headers
    });
}

/**
 * @param {Request} request
 * @returns {Response}
 */
export default function middleware(request) {
    const pathname = reroute({ url: new URL(request.url) });
    return rewrite(new URL(pathname, request.url));
}

EDIT: Added a draft PR below.