remix-run / remix

Build Better Websites. Create modern, resilient user experiences with web fundamentals.

Home Page:https://remix.run

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Assets path are conflicting with routing

ric980 opened this issue · comments

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/abc1-s@1x.webp. The images for the products are located in public/images/products/....

And this is the error from Sentry:

URL = http://shop:3000/images/products/abc1-s@1x.webp (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.