Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redirect preserve query string #11

Open
joknoxy opened this issue Jul 20, 2021 · 11 comments
Open

Redirect preserve query string #11

joknoxy opened this issue Jul 20, 2021 · 11 comments

Comments

@joknoxy
Copy link

joknoxy commented Jul 20, 2021

This relates to issue #7 . I've used the code from there and made it more generic and fixed a couple of edge-cases that wouldn't have worked. Please add this to sample library as it's a very common use-case with a non-obvious solution (for those of us not js experts).

function objectToQueryString(obj) {
    var str = [];
    for (var param in obj)
        if (obj[param].value == '') 
            str.push(encodeURIComponent(param));
        else 
            str.push(encodeURIComponent(param) + "=" + encodeURIComponent(obj[param].value));   
        
    return str.join("&");
}

function handler(event) {
    var request = event.request;
    var uri = request.uri;
    var loc = "";
    var newdomain = newdomain.com;

    if (Object.keys(request.querystring).length) 
        loc = `https://${newdomain}${uri}?${objectToQueryString(request.querystring)}`
    else 
        loc = `https://${newdomain}${uri}`

    var response = {
        statusCode: 302,
        statusDescription: 'Found',
        headers: {
            'location': { value: `${loc}` }      
        }
    };
    return response;
}

@akarsh-k
Copy link

akarsh-k commented Aug 17, 2021

@joknoxy do we need to handle the case where the query string has multi values?
I have modified objectToQueryString as shown below.

function objectToQueryString(obj) {
  var str = [];
  for (var param in obj)
      if (obj[param].multiValue)
          str.push(encodeURIComponent(param) + "=" + encodeURIComponent(obj[param].multiValue.map((item) => item.value).join(',')))
      else if (obj[param].value == '') 
          str.push(encodeURIComponent(param));
      else 
          str.push(encodeURIComponent(param) + "=" + encodeURIComponent(obj[param].value));   
      
  return str.join("&");
}

While checking further, I noticed an issue with this approach. If the query string is already an encoded string, we will be encoding it again in the cloudfront function. So I end up using this:

function objectToQueryString(obj) {
      var str = [];
    for (var param in obj)
        if (obj[param].multiValue)
            str.push(param + "=" + obj[param].multiValue.map((item) => item.value).join(','));
        else if (obj[param].value == '')
            str.push(param);
        else
            str.push(param + "=" + obj[param].value);

    return str.join("&");
}

@goetzc
Copy link

goetzc commented Nov 15, 2021

While checking further, I noticed an issue with this approach. If the query string is already an encoded string, we will be encoding it again in the cloudfront function. So I end up using this:

Thank you @akarsh-k, this works perfectly with the CloudFront event example structure.

@edzis
Copy link

edzis commented Jan 12, 2022

We ended up using a slight modification that preserves original grouping of multiValues and has some documentation:

/**
 * Patches lack of
 * https://developer.mozilla.org/en-US/docs/Web/API/Location/search in event.
 * Inspired by
 * https://github.com/aws-samples/amazon-cloudfront-functions/issues/11.
 * @param obj The weird format exposed by CloudFront
 * https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-event-structure.html#functions-event-structure-query-header-cookie
 * @returns {string} Tries to return the same as
 * https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/toString
 */
function getURLSearchParamsString(obj) {
  var str = [];
  for (var param in obj) {
    if (obj[param].multiValue) {
      str.push(
        obj[param].multiValue.map((item) => param + "=" + item.value).join("&")
      );
    } else if (obj[param].value === "") {
      str.push(param);
    } else {
      str.push(param + "=" + obj[param].value);
    }
  }
  return str.join("&");
}

Now
?aa=11&bb=22&aa=33&bb=44,55&cc=66&dd&ee
results in
?aa=11&aa=33&bb=22&bb=44,55&cc=66&dd&ee
instead of
?aa=11,33&bb=22,44,55&cc=66&dd&ee

@longzheng
Copy link

longzheng commented May 2, 2023

Adapted JSDoc types from https://www.npmjs.com/package/@types/aws-lambda

/**
 * Patches lack of
 * https://developer.mozilla.org/en-US/docs/Web/API/Location/search in event.
 * Inspired by
 * https://github.com/aws-samples/amazon-cloudfront-functions/issues/11.
 * @param {import("aws-lambda"). CloudFrontFunctionsQuerystring} querystring The weird format exposed by CloudFront
 * https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-event-structure.html#functions-event-structure-query-header-cookie
 * @returns {string} Tries to return the same as
 * https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/toString
 */
function getURLSearchParamsString(querystring) {
    var str = [];

    for (var param in querystring) {
        var query = querystring[param];
        var multiValue = query.multiValue;

        if (multiValue) {
            str.push(multiValue.map((item) => param + '=' + item.value).join('&'));
        } else if (query.value === '') {
            str.push(param);
        } else {
            str.push(param + '=' + query.value);
        }
    }

    return str.join('&');
}

@edzis
Copy link

edzis commented Jun 8, 2023

@longzheng Instead of CloudFrontFunctionsEvent I believe it should be CloudFrontFunctionsEvent['request']['querystring'].

@longzheng
Copy link

@longzheng Instead of CloudFrontFunctionsEvent I believe it should be CloudFrontFunctionsEvent['request']['querystring'].

Sorry I made a typo when I edited it, it can also be CloudFrontFunctionsQuerystring since that's what CloudFrontFunctionsEvent['request']['querystring'] references. I'll update my comment.

@cornwe19
Copy link

cornwe19 commented Nov 1, 2023

It's especially confusing that AWS offers a querystring helper package which exposes a stringify method that doesn't actually work with their own representation of the event.request.querystring object https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-javascript-runtime-features.html#writing-functions-javascript-features-builtin-modules-query-string

@lukevers
Copy link

lukevers commented Jan 24, 2024

okay so I updated this a bit on my end because I was worried about double encoding, but also worried about not having encodings properly in query params. I'm also not redirecting here like in the example above. this was super helpful though, thank you @joknoxy !

function isEncoded(uri) {
  uri = uri || '';
  return uri !== decodeURIComponent(uri);
}

function fullyDecodeURI(uri){
  while (isEncoded(uri)) {
    uri = decodeURIComponent(uri);
  }

  return uri;
}

function encode(param) {
  return encodeURIComponent(fullyDecodeURI(param));
}

function objectToQueryString(obj) {
  var str = [];

  for (var param in obj) {
      if (obj[param].multiValue) {
          str.push(encode(param) + "=" + obj[param].multiValue.map((item) => encode(item.value)).join(','));
      } else if (obj[param].value == '') {
          str.push(encode(param));
      } else {
          str.push(encode(param) + "=" + encode(obj[param].value));
      }
  }
      
  return str.join("&");
}

function handler(event) {
  var request = event.request;
  request.headers["x-forwarded-host"] = request.headers.host;
  request.querystring = objectToQueryString(request.querystring);
  return request;
}

@karpolan
Copy link

karpolan commented Feb 1, 2024

Thanks for the comments, it helps me to solve my issue.

Here is my code for redirect with query params + adding missing /index.html for SPA or SSG websites

https://gist.github.com/karpolan/ecce9c372bebb448ee04cc240ca5c8aa

@Akshay090
Copy link

Akshay090 commented Apr 25, 2024

thanks @longzheng for sharing #11 (comment), got this working thanks to it 🚀

for anyone else trying to build
cloudfront function to redirect to non trailing slash url while preserving query string
refer 👇

function handler(event) {
    // Get the request object from the event
    var request = event.request;

    // Get the URI of the requested resource
    var uri = request.uri;

    // Get the query string parameters
    var queryStringParameters = request.querystring;

    // Remove all trailing slashes from the URI using a regular expression
    var newUri = uri.replace(/\/+$/, "");

    // Check if the URI had trailing slashes
    if (newUri !== uri) {
        // Check if querystring is not empty
        var hasQueryString = Object.keys(queryStringParameters).length > 0;

        // Construct the new URI without the trailing slashes and include the query parameters if querystring is not empty
        var redirectUri = newUri + (hasQueryString ? '?' + getURLSearchParamsString(queryStringParameters) : '');

        // Redirect to the new URI without the trailing slashes
        var response = {
            statusCode: 301,
            statusDescription: "Moved Permanently",
            headers: {
                location: { value: redirectUri },
            },
        };

        return response;
    }

    // If there's no trailing slash, proceed with the request as is
    return request;
}

// Helper function to format query string parameters
function getURLSearchParamsString(querystring) {
    var str = [];

    for (var param in querystring) {
        var query = querystring[param];
        var multiValue = query.multiValue;

        if (multiValue) {
            str.push(multiValue.map((item) => param + '=' + item.value).join('&'));
        } else if (query.value === '') {
            str.push(param);
        } else {
            str.push(param + '=' + query.value);
        }
    }

    return str.join('&');
}

@jamesflores
Copy link

This relates to issue #7 . I've used the code from there and made it more generic and fixed a couple of edge-cases that wouldn't have worked. Please add this to sample library as it's a very common use-case with a non-obvious solution (for those of us not js experts).

This saved me! I kept getting ?[object%20Object]

Is this issue still open?!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants