Catch a Missing Query Parameter Rewrite and Serve a 404

Let’s say I want to mimic an API and serve some JSON files. I have a file structure like this:

├── candy-bars/
    ├── twix.json
    ├── kitkat.json
    ├── snickers.json
    ├── 404.json
    └── index.json

I want people to be able to ask for all the candy bar data at /candy-bars or to just ask for a specific candy bar with a query param, e.g. /candy-bars?name=kitkat

So I write the following in my _redirects file:

# Any request to `?name=xxx` serves that file on disk
/candy-bars name=:name /candy-bar/:name.json 200

# If `?name=xxx` matches but the corresponding file is missing, serve a 404
/candy-bars name=:missing /candy-bar/404.json 404

# For the bare resource, serve the index
/candy-bars /candy-bars/index.json 200

This works well for calls to /candy-bars and candy-bars?name=kitkat

But for a query param that doesn’t match a file on disk, it fails, e.g. /candy-bars?name=reeses doesn’t work because there is no /candy-bars/reeses.json file on disk. Instead it serves the generic 404.html page in my root.

I think this is because Netlify sees the first redirect, ?name=reeses and says “oh I have a match” even though the reeses.json file isn’t on disk anywhere. In that case, I half-expected Netlify to then keep going down the list of redirects and find another rule that not only matches but also has a file to serve. However, that doesn’t appear to be the case. I’m guessing once it finds a match, it won’t process anymore rules even if the file you’re trying to serve doesn’t exist.

As you can imagine, that makes dynamic query params difficult because ?name=:name means somebody could type anything as the name value and you might have something for that but you might not and there’s no way to catch the case where you might not?

Is there a way around this? Would there be a proper way to accomplish what I’m trying to do (without having to write a function that handles it)?

Hey @jimniels

Using redirects won’t work for this (at least as I understand and have tested.) You could use Edge Functions.

Here’s a working test function which works locally and does what I believe you want.

export default async({url}, ctx) => {

  const params = new URL(url).searchParams

  const name = params.get('name')
  if (name) {

    const response = await`candy-bar/${name}.json`)

    if (response.status === 200) {
      return new Response(response.body, response)

    return ctx.rewrite('/candy-bar/404.json')

  else {
    return ctx.rewrite('/candy-bar/index.json')


I haven’t played with edge functions yet, maybe I’ll give that a try. I was hoping to be able to solve this with some kind of hidden redirect magic, but looks like this is the next prescribed way to handle this sort of thing.