aws-samples / amazon-cloudfront-functions

Apache License 2.0
472 stars 75 forks source link

Redirect preserve query string #11

Open joknoxy opened 3 years ago

joknoxy commented 3 years ago

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 commented 3 years ago

@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 commented 2 years ago

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 commented 2 years ago

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 commented 1 year ago

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 commented 1 year ago

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

longzheng commented 1 year ago

@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 commented 10 months ago

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 commented 8 months ago

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 commented 7 months ago

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 commented 5 months ago

thanks @longzheng for sharing https://github.com/aws-samples/amazon-cloudfront-functions/issues/11#issuecomment-1530752418, 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 commented 2 weeks ago

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?!