Assets path are conflicting with routing
Reproduction
It hard to reproduce as it happens infrequently, maybe several times a day, but I'll explain the issue in very detail. I cannot reproduce it locally. But it is reported by Sentry a few times a day.
This is my route: app/routes/($lang).products.$id.
And this is my image path: /images/products/[email protected]. The images for the products are located in public/images/products/....
And this is the error from Sentry:
URL = http://shop:3000/images/products/[email protected] (where abc1 is the ID of the product)
Error = GET routes/($lang).products.$id
The actual error from my ($lang).products.$id.tsx component is Error Invariant failed., because it cannot find a product with that ID.
Why the router matches a path for my assets in the public directory? I understand that this regex $param.products.$param matches this URL /images/products/abc, but it shouldn't. I am asking for an image here.
Please, help! This happens 300 times by now and users cannot see the images. But when they refresh the page - it works.
System Info
System:
OS: Alpine Linux 3.19.1
Binaries:
Node: 20.12.1
Yarn: 4.0.2
npm: 10.2.4
npmPackages:
@remix-run/dev: ^2.9.1 => 2.9.1
@remix-run/node: ^2.9.1 => 2.9.1
@remix-run/react: ^2.9.1 => 2.9.1
@remix-run/serve: ^2.9.1 => 2.9.1
Used Package Manager
yarn
Expected Behavior
I expect that assets paths won't conflict with routing.
Actual Behavior
Assets paths conflicting with routing.
Are you using remix-serve? If so then I think this is due to the default express.static behavior which sets fallthrough:true b default, so if a path doesn't match inside public/ it continues and tries to match that path via subsequent handlers which in this case would be the Remix handler.
remix-serve source code link for reference: https://github.com/remix-run/remix/blob/main/packages/remix-serve/cli.ts#L135
One quick and easy solution is to check params.lang against an accepted list of language codes and return a 404 to short circuit your loader.
If you need more control, you can implement your own express server and set up an express.static handler for /images that has fallthrough:false so it never hits the Remix handler.
Thanks for your help. How to do this:
One quick and easy solution is to check params.lang against an accepted list of language codes and return a 404 to short circuit your loader.
I think what he means is that in your loader for the ($lang).products.$id.tsx route
export async function loader({ request, params }: LoaderFunctionArgs) {
validateLangParam(params.lang)
// ... continue
}
function validateLangParam(lang: string) {
if (lang === 'images') {
throw new Response('Not Found', { status: 404 })
}
// or you can do
if (lang && !(['en', 'fr', 'es', 'de'].includes(lang))) {
throw new Response('Not Found', { status: 404 })
}
}
Thanks, but I definitely don't want to do this in every loader.
Also, hit a new problem. I have these routes:
($lang).contact.tsx
($lang).thanks.$id.tsx
As the lang is optional, now this path /thanks/contact matches this route ($lang).contact.tsx. You should be more greedy when doing regexes. Exact matches should go first. Now I have to rework my URLs to get around all these routing issues.
Is there any way to do something like this:
(en|de).contact.tsx
(en|de).thanks.$id.tsx
Good job
I would recommend putting your assets in an assets/ directory that you can use at the express layer to serve via express.static and never let them fall through to your remix handler.
Thanks, but I definitely don't want to do this in every loader.
You don't have to do it in every loader, you could do it in your root or or a ($lang).tsx loader and redirect to a proper default lang prefix to avoid the lang prefix consuming /thanks.