queryStringParameters not working with redirect on production

@jen unfortunately the other post covers 301/302 redirects and how they handle query parameters (bringing their behavior in line with the 200 redirect that’s being used here)

the issue here is different: the rewrite rules in local dev environment allow captured path parameters to be rewritten arbitrarily into the target URL, whereas in production this does not happen (i.e. moving the :msg parameter from path to query param does not happen)

I personally really like the dev environment behavior, as it saves a lot of unnecessary path re-parsing in my function code (the original path does get passed in the event AFAIK, so it is possible to add a middleware to re-parse it, but that seems silly given that the rewrite already has the necessary information), so I’d love to see it enabled in production

however, if that is not possible it should disabled on dev so the behavior is consistent – it’s possible that it automagically works because of some express functionality that netlify dev (but not netlify’s production env) uses

I think you hit the nail on the head with your conclusion. The cli is more an emulation of how Netlify works in production. I do feel that it would be easier to change the behavior on the cli vs the production code at this time. Would you mind filing an issue about that here: https://github.com/netlify/cli/issues?

Opened a bug here: https://github.com/netlify/cli/issues/1605

FWIW I still think the production behavior is a huge limitation, as functions must be aware of their paths to be able to parse their parameters, which is brittle, verbose, and not DRY. I am almost tempted to stand up an nginx proxy to handle these rewrites for me (to keep the path parsing logic out of the function code), but that would nullify the advantages of netlify’s infrastructure.

I think the ideal behavior would be for {path} parameters in redirects to automatically populate the pathParameters property on APIGatewayEvent, as they do on AWS API Gateway.


thanks for opening an issue on that repo, @parkan. appreciate it!

I’m troubleshooting something very similar, but I think even more basic in that I’m not passing a variable in from to to and I still have the same issue. Everything works on local with netlify dev but when I push to a preview branch it doesn’t work.


Given this rule in my _redirects file:

/api/v1/search /.netlify/functions/api-v1-search?siteId=macos 200

With netlify dev on my local machine, event.queryStringParameters.siteId in my function is macos.

When pushed to a preview branch, event.queryStringParameters.siteId in my function is undefined.

More Context

I want to include a specific query parameter for a given domain, so the value gets hardcoded into the redirect itself at build time. I have a siteId variable that gets set in the build. So my _redirects template file looks like this:

/api/v1/search /.netlify/functions/api-v1-search?siteId=${siteId} 200

When this gets built, the resulting _redirects file on disk puts out a redirect rule like this:

/api/v1/search /.netlify/functions/api-v1-search?siteId=macos 200

The weird thing is, like others in this thread, the way that rule functions seems to differ between what i’m running locally with netlify dev and what is running in a preview branch build even though the redirect rule is the exact same.

Local redirect

When I run things locally using netlify dev, I get a _redirects file on disk with this rule:

/api/v1/search /.netlify/functions/api-v1-search?siteId=macos 200

Great. My function actually echos back the URL that was hit, so when i hit /api/v1/search on local using netlify dev, it gives me back this in the response:

"links": {
  "self": "https://www.macosicongallery.com/api/v1/search"

Cool. Everything works as expected locally. So I push to a preview branch.

Preview Branch redirect

So now i have the same code that was working locally, but now being built remotely by netlify’s servers and on a preview branch. Through some logging in the build, I’ve confirmed that the rule in the _redirects file is the exact same one in the local build:

/api/v1/search /.netlify/functions/api-v1-search?siteId=macos 200

But when i hit /api/v1/search on this remote preview URL – https://5fd447f2a818530007016641--macosicongallery.netlify.app/api/v1/search – it doesn’t echo back the same thing when I did it locally. I get undefined in place of where i was getting macos on my local build:

"links": {
  "self": "https://www.undefinedicongallery.com/api/v1/search"

To me, this means that the request coming into the lambda function isn’t detecting a siteId query paramenter. But i’m unsure how/where I’m doing the wrong thing, especially since it’s working locally using netlify dev (although, it sounds like from this thread, netlify dev is just an approximation, which is obviously terribly misleading in this case).

Given what Netlify’s docs say:

Query string parameters are not automatically passed to our redirect engine.

I’m still not sure why this rule isn’t working:

/api/v1/search /.netlify/functions/api-v1-search?siteId=macos 200

I’m not trying to passing query params from /api... to /.netlify/functions.... I’m just tacking on a hard-coded query param in the to, which I’m expecting to be passed to the invoked lambda. But somehow it’s being set to undefined on my preview branch.

What a great post, you clearly have the “Support Engineer mentality” in specifying your experiments and results very clearly!

The docs are a bit confusing there since we’re halfway through this work: Changes to Redirects with query string parameters are coming but TL;DR the “200” redirect is more of a “rewrite” and there query string params ARE intended to be passed on as is without a specific param1=:value1 as documented here: https://docs.netlify.com/routing/redirects/redirect-options/#query-parameters.

I think you’re saying that they are not, in fact, passed on - and I think that may be due to a bug around how we cache - we don’t always cache separately based on param, so you may have gotten a result from a prior param-less request when you made yours maybe? But, I am not so sure of that diagnosis. Could you let me know if your function is still deployed somewhere I can access it, to do some testing to see if I can figure things out? (I get a 404 at the presumed prod URL https://www.macosicongallery.com/api/v1/search)

1 Like

the query string params ARE intended to be passed on as is without a specific param1=:value1 as documented.

I think you’re saying that they are not, in fact, passed on

That’s precisely right. They do not appear to be getting passed on. Not only that, but my rule isn’t even trying to capture anything to pass along! It’s just saying redirect one path to another path and include a hard-coded parameter i.e. /path /otherpath?q=value

I have not yet deployed to production—precisely because of this bug—but have a feature branch build of the feature. You can access it here.

Of note:

I added a log to my build process which outputs the contents of my _redirects file. Looking at the build for the above linked feature branch, I can see the output of my _redirects file includes the proper rule:

Notice the rule?

/api/v1/search /.netlify/functions/api-v1-search.js?siteId=macos 200

As I would expect given what’s documented about the redirects/rewrites engine, that should be passing on any query params. But if you do a GET to the following URL:


The lambda will return an error because it’s expecting a siteId parameter but isn’t getting one.

If you include siteId explicitly in a GET request, i.e.:


It works as expected. But, as noted, I’m not expecting to have to pass that parameter explicitly given my rewrite rule, which doesn’t appear to be functioning as expected.

Hi @jimniels,

Yea, I think that was due to the fact that the code that was enabling the pass-through was reverted but has since been re-fixed. Can you confirm that this is the case?

For some context, the post mentions the fix was re-deployed 8 days ago.

@Dennis nope, still not working for me.

Just ran a rebuild of my site above. My build outputs my redirect, which looks like this:

3:35:39 PM:   '/api/v1/search /.netlify/functions/api-v1-search?siteId=macos 200', 

You can see my rule is saying to redirect to another path with a hard-coded query param (i.e. /path to /.netlify-fn?foo=bar) but the query param is not showing up in the lambda.

You can test this at the preview build. Do a GET to:


It doesn’t work. You’ll see the siteId parameter is coming back from the lambda function as undefined. If you hard code it in the GET, it works:


So the redirect is still not working. Or maybe I’m missing something?

I feel like I must be missing something really basic here?

All I’m doing is a redirect from a path to a new path with a (hard-coded) query string param.

/path/to/resource /redirect/path?with=query 200

How does that not work?

Following up with more context. Hoping @fool or @Dennis can help me out.

So I’ve been playing with this a bit more to try and figure it out. I’ve found that doing a regular redirect rule works as expected. But using rewrites doesn’t work as I would expect.

Note: all of these examples are using my preview build at: https://5ff7956eba84190008348a88--macosicongallery.netlify.app

Redirects (301s)

Here’s the redirect rule in my _redirects file:

/netlify-support-test-no-200 /404.html?foo=bar

Because I didn’t specify a HTTP code, it uses a 301 and redirects the user from one path to another with the specified query params.

If I curl that resource, I can see I get a 301 redirect to the 404 page with the query parameters.

If I hit that URL in my browser – /netlify-support-test-no-200 – it redirects to the 404 page with the query parameters in the URL bar.

Screen Shot 2021-01-07 at 4.19.04 PM

Cool, that works just as expected.

Rewrites and proxies (200s)

If I setup a URL to rewrite with a 200, then I don’t see the query parameters show up. Here’s the rule in my _redirects file:

/netlify-support-test /404.html?foo=bar 200

If I curl this resource, it returns a 200 with the contents of the 404 page. That makes sense:

If I hit it directly in my browser (/netlify-support-test) I see the contents of the 404 page (same as the redirect above) but my browser’s URL doesn’t change (and doesn’t have the query params in it)

Screen Shot 2021-01-07 at 4.24.22 PM

That all makes sense. Because this is a rewrite, Netlify is making a request to the URL with the query params behind the scenes, so any query params are hidden from the URL bar and from the curl.

But that request that’s happening behind the scenes isn’t working as expected

I have one more rewrite rule setup. This one is designed to be a rewrite that points to a URL with query params. Here’s the rule:

/netlify-support-test-params /.netlify/functions/netlify-support-test?foo=bar 200

Note that this is pointing at a netlify (lambda) function with explicit query parameters. The lambda function’s code is really simple. It’s essentially echoing back the query parameters.

Screen Shot 2021-01-07 at 4.45.14 PM

So my expectation is that if I curl /netlify-support-test-params I’ll get a 200 and it will echo back {"query": {"foo": "bar"}. But it does not. The lambda receives no query parameters:

However, if I do include some query params myself in the request, it echoes those back. For example, if I curl /netlify-support-test-params?hello=world, I get a 200 with {"query":{"hello":"world"}} echoed back to me:

What gives?

Is my expectation incorrect here? It appears Netlify’s rewrites/proxy engine is not actually making a request with query params in tow. To restate:

Given this rule: /path /rewrite?with=params 200

I expect: a request to /path will result in Netlify making a request to /rewrite?with=params behind the scenes and then returning a 200 result to /path.

But what I get is that a request to /path results in, apparently, Netlify making a request to /rewrite without any query params and returning a 200 result to /path.

1 Like

Hey there,
We put this question to the team who works on our proxy and they explained that rewrites to functions work differently than rewrites to other paths. They suggested that you could accomplish what you want by grabbing the incoming path (/.netlify/functions/netlify-support-test?foo=bar) directly in your function using event.path and then parsing the param you’re interested in within your function. Sorry for the confusion here. Let us know if that solution would work for you.

rewrites to functions work differently than rewrites to other paths

Well that’s interesting. That must be the source of my problem.

They suggested that you could accomplish what you want by grabbing the incoming path…directly in your function using event.path and then parsing the param you’re interested in within your function.

That doesn’t work. Or maybe I’m misunderstanding the suggestion?

Let’s say I have the following redirect:

/thing /.netlify/functions/thing?foo=bar 200

If I do a GET to /thing that results in the invoked function’s event.path being /thing, which as I type that I guess makes sense in the context of function calls. Given that case, however, there are no query parameters present and it appears impossible for a function call to know about its /.netlify/functions/path along with any query params that might be present, unless you do a GET to it directly.

So—and I’m writing out loud here—it sounds like my expectation is completely wrong. Given the redirect above, I would expect a GET to /thing to proxy a call to the netlify function URL, which would result in the function having even.path be /.netlify/functions/thing?foo=bar, which I would then be able to parse. But that’s not how it’s working. The location that is being proxied to—the /.netlify/... url—is completely hidden from the invoked function.

Thus, from what I can tell in the implementation, it seems impossible to add query string parameters to a redirect for a function call, i.e. even given a hard-coded redirect of

/path /.netlify/functions/path?foo=bar 200

It’s impossible for the invoked function to know anything about foo=bar.


I suggested the workaround of using the request path in the function, and I’ll try to explain it a bit better.


Yes, the event.path in a function will always be the path of the original incoming request.
This is mostly legacy behaviour that was implemented like this once and we can’t really change it now without breaking people’s existing functions.

Primary use case: This means that you can use redirects to make Netlify invoke a function for any path. Great, right?

Guessing your use case

I’m guessing what you want to do is rewrite to a function from different paths and pass some data along in the different redirects based on which rule was matched, so that the function can behave differently using that added context.
Yes, this is where the redirects mechanism falls a bit short.

Solution 1: Do the matching inside your function

Since our redirect mechanism is not able to provide you the data of which rule it matched, you could try to match the original request in your function to determine what it should do.



/first-thing /.netlify/functions/the-only-function 200!
/second-thing /.netlify/functions/the-only-function 200!


exports.handler = async function(event, context) {
    if (event.path === "/first-thing") {
        // do one thing
    } else if (event.path === "/second-thing") {
        // do the other thing
    } else {
        // return 400 status, because the function was invoked raw

Solution 2: Proxy headers hack

When you rewrite something you can add custom proxy headers (click for docs).

The same example solved using those:


from = "/first-thing"
to = "/.netlify/functions/the-only-function"
status = 200
force = true
headers = {X-Variant = "first-thing"}

from = "/second-thing"
to = "/.netlify/functions/the-only-function"
status = 200
force = true
headers = {X-Variant = "second-thing"}


exports.handler = async function(event, context) {
    if (event.headers["X-Variant"] === "first-thing") {
        // do one thing
    } else if (event.headers["X-Variant"] === "second-thing") {
        // do the other thing
    } else {
        // return 400 status, because the function was invoked raw

Hope this helps you solve your specific issue!

1 Like


Wanted to chime in here too with another option that may make for some better flexibility — in addition to @marcus’s premise of having your rewrite/redirect entry points being unique paths, e.g. /first-thing and /second-thing, you can use query string parameters on the entry-point side of even a rewrite to a Function and those will be available within the Function.

Meaning I can have a rewrite of

/only-thing /.netlify/functions/my-one-function 200!

and send a request to example.com/only-thing?image=the_big_one.jpg

then my function’s event.path will be /only-thing and event.queryStringParameters will be { "image": "the_big_one.jpg" }.

Just more options for you :slight_smile:


Thanks for the feedback. Going to add some context for anybody who ends up here in the future.

Clarifying My Misunderstanding

I can see now that my original confusion was around the redirect functionality as it applies to functions. To quote @Marcus

a function will always be the path of the original incoming request

So if I have a redirect to a function like this:

/foo /.netlify/functions/bar?with=param 200

And I do a GET to /foo, the lambda will be involved with event.path as foo. Hard-coded query params in the rewrite don’t show up.

Honestly, now as I type it out, it makes sense. I had mistakenly assumed the function call would note its path as being its canonical URL (i.e. always the /.netlify/functions/...) rather than whatever path it was originally called at, which makes sense. I could see use cases on either side, but I would venture my case is the outlier.

Solution to My Problem

What I need is some extra data about the environment. The suggestion from @Marcus for headers is more aligned with what I need.

The production host name contains a piece of information I need. When I call a function, that shows up in event.headers.host, so I can parse that and find the info I need. The problem there is that when I’m on localhost that piece of info is gone. So I need a solution for local testing. This is easy enough now to just hard-code. But in the future, I’ll probably look into a more dynamic solution. Something along the lines of the using a custom header, i.e. X-My-Value. The complexity there is that I need that value to be based on an env variable and it looks like you can use env variables in netlify.toml but it requires extra complexity.

Thanks everyone for the clarification!

1 Like

Hey folks, I happened across an alternative workaround that might be helpful for some of you. It involves setting an additional redirect in front of the rewrite to the function.

For example, say you wanted to take a path like /img/kitten.jpg?width=640 and send it to your img-resizer function with the filename and width passed in event.queryStringParameters.

You could first transform the path with a 301 redirect with placeholders:

/img/:filename width=:width  /resized-img?file=:filename&w=:width&q=75   301

That rule changes /img/kitten.jpg?w=640 to /resized-img?file=kitten.jpg&width=640. Note that I was able to do three things with this rule:

  • converted part of the path to a query parameter
  • changed the name of an existing parameter
  • added an additional hard-coded parameter

Now you can pass that transformed path to the function endpoint:

/resized-img  /.netlify/functions/img-resizer  200

There’s no need to specify the query parameters because they are passed to the function event automatically.

:warning: Note that while this second rule can be a 200 rewrite, the first one cannot. The first rule must be a 301 (or 302) redirect because the URL has to change in order for the redirect parser to be triggered a second time.

Now, with those two rules in place, If I enter https://my-site.netlify.app/img/kitten.jpg?width=640 into a browser address bar, the following will happen:

  1. The address bar URL will change to
  2. My img-resizer will be invoked with the following values passed in the event parameter:
      path: '/resized-img',
      queryStringParameters: { file: 'kitten.jpg', q: '75', w: '640' },
      ... other values ...
  3. When the img-resizer function finishes, it will return its result to the browser. (Presumably the function has been written to return a resized image. :wink: )

With this image example, it’s worth noting that the whole double-redirect thing doesn’t require the path to be entered in the browser address bar. It works with inline references in code, too. So if I had an img tag in my page, like

<img src="/img/kitten.jpg?width=640"/>

It will still return the result of the img-resizer function in that img tag. You could do the same thing with, for example, a json file request in client-side JavaScript.


Interesting! :thinking: @verythorough have you happened to check if the fetch api follows 301s by default so that this approach could be used for JS calls?


The default redirect policy on fetch is follow.

A RequestRedirect enum value, which can be one the following strings:

  • follow
  • error
  • manual

If not specified when the request is created, it takes the default value of follow .

See Request.redirect - Web APIs | MDN

1 Like

Great! Seems like that may be a viable work-around! Thanks @verythorough :raised_hands:t2: