npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

middy-reroute

v2.5.4

Published

A middyjs middleware implementation of Netlify's redirects functionality.

Downloads

18

Readme

TOC

Why?

Serverless hosting of static websites presents a few challenges when it comes to typical server http responses. What we really want as developers it to have the power of .htaccess or similar functions. This allowed you to create complex redirect and rewrite rules and simply by placing this file alongside your application files you'd be off to the races.

So far, no cloud provider offers this ease of configuration without a bit of setup. In the case of AWS, you can use Lambda@Edge to intercept all incoming requests and to handle them as you like. middy-reroute aims to simplify that process by handling all the heavy lifting and offering you a familiar _redirects file with which to implement your desired redirects, rewrites, proxies, and custom 404s.

This functionality is modeled very closely after a similar core feature of Netlify's hosting service.

Install

As simple as:

npm install middy middy-reroute

Requirements

  • middy
  • Node >= 6
  • Lambda@Edge
    • Cloudfront -> Lambda -> S3
    • origin-request event
  • (Optional) Whitelist Host, Accept-Language, and CloudFront-Viewer-Country headers on Cloudfront CDN. These will allow you to use Host, Language and Country specific conditions in the rules.

middy-reroute has been built to work by default from Node >= 6. You will also need the core middyjs middleware engine.

While most of middy's middlewares have been written for use with AWS Lambda + API Gateway, middy-reroute work exclusively with Lambda@Edge. This is due to the fact that the middleware needs to run at the edge CDN so that it can intercept requests made to your origin (S3).

Not familiar with Lambda@Edge? No worries, it's easier then it sounds, and there are many great tutorials and courses. Full disclosure, I'm the author of this course.

Usage

Using middy-reroute is pretty effortless but very powerful.

  1. Create your Lambda function similar to the handler.js below.
  2. Associate this function with the origin-request event of your CloudFront CDN that sits in front of your site's s3 bucket.
  3. IF you want to take advantage of domain/host specific routing.
    1. Whitelist the Host header on the above CDN.
  4. Place your desired rules into a _redirects file and upload into the root of your S3 bucket.
  5. Profit!

Example:

# handler.js

const middy = require('middy');
const { reroute } = require('middy-reroute');

// This is your typical handler. The only difference here is that you know that the event you get will already be manipulated by the middy-reroute middleware.
const finalHandler = (event, context, callback) => {
  // middy-reroute will always return three types of events
  // 1) full original event untouched
  // 2) full original event with the [uri] changed
  // 3) raw response which may be a redirect or a response that includes the body etc. Eg. Custom 404, proxied response etc.
  const request = !!event.Records ? event.Records[0].cf.request : event;

  // Unless you have additional operations, you would normally just return middy-reroute's event untouched.
  callback(null, request);
}

const handler = middy(finalHandler)
  .use(reroute());

module.exports = { handler };

_redirects file

See Netlify's docs for more detailed information about options.

# Redirect with 301
/home           /
/google         https://www.google.com

# Redirect with 302
/my-redirect    /             302

# Rewrite a path
/pass-through   /index.html   200
/*              /index.html   200

# Custom 404
/ecommerce      /closed       404

# Placeholders
/news/:year/:month/:date/:slug    /blog/:date/:month/:year/:slug

# Splats
/news/*   /blog/:splat

# Proxying
/api/*  https://api.example.com/:splat  200

# Country
/  /china   302  Country=cn,hk,tw
/  /israel  302  Country=il

# Language
/china/*  /china/zh-cn/:splat  302  Language=zh

# UserAgent
/*  /upgrade-browser  200!  UserAgent=IE:<=11
/*  /great-choice     200!  UserAgent=Chrome:*,Firefox:*

Country codes should be iso3166 and language codes the proper format.

For languages, note that en will match en-US and en-GB, zh will match zh-tw and zh-hk, etc.

API

options

{
    // 'file' is the S3 key where your rules file can be found
    file: '_redirects',  // default

    // 'multiFile' is a boolean to denote if you would like middy-reroute to
    // look for host specific rules files or just a single rules file matching
    // the *options.file* param above. MultiFiles will resolve to 
    // `${options.file}_${host}`
    // Eg. multiFile: true (and the host is domain.com)
    //     _redirects_domain.com
    multiFile: false,  // default

    // 'rulesBucket' is the bucket to reference when looking for the rules files
    // Default: As a default the origin bucket is used.
    rulesBucket: 'my-existing-bucket-name',

    // 'regex'
    regex: {
      // 'htmlEnd' is used to identify URI that end in an html file.
      // These will be rewriten if *options.friendlyUrls*: true.
      htmlEnd: /(.*)\/((.*)\.html?)$/,  // default

      // 'ignoreRules' is used to identify lines in the rules file that should
      // be completely ignored.
      // Default: Ignores comments and empty lines.
      ignoreRules: /^(?:\s*(?:#.*)*)$[\r\n]{0,1}|(?:#.*)*/gm,  // default

      // 'ruleline' is used to parse each individual line of rules in the rules
      // file. Change this at your own discretion. At the very lease the same 
      // number and order of match groups needs to be defined.
      ruleline: /([^\s\r\n]+)(?:\s+)([^\s\r\n]+)(?:\s+(\d+)([!]?))?(?:(?:\s+)?([^\s\r\n]+))?/,  // default

      // 'absoluteUri' is used to identify if a URL is absolute vs relative.
      //  /this  =  false
      //  http://domain.com/this   =  true
      absoluteUri: /^(?:[a-z]+:)?\/\//,  // default
    },

    // 'defaultStatus' - specifies the default http status to use when none is
    // specified in the rule.
    defaultStatus: 301,  // default

    // 'redirectStatuses' declares which http statuses should result in
    // redirects.
    redirectStatuses: [301, 302, 303],  // default

    // 'friendlyUrls' specifies whether the URIs should be redirected 
    // to avoid ending in .html
    // Eg. 
    //   /thing/index.html  =  /thing/
    //   /thing/about.html  =  /thing/about/
    friendlyUrls: true,  // default

    // 'defaultDoc' is the name of the file that should be served up when
    // paths are referenced.
    // Eg. /thing  will be rewriten to /thing/index.html
    // note* Since it's a rewrite the user will still see this as /thing
    defaultDoc: `index.html`,  // default

    // 'custom404' is the S3 key where to automatically look for your custom
    //  404 page when a resource can't be found.
    custom404: `404.html`,  // default

    // 'cacheTtl' is the TTL in seconds that S3 calls should be cached.
    //  Eg. get rules file or custom404
    //  set to 0 to disable caching
    cacheTtl: 300, // secounds default

    // 'incomingProtocol' is the protocol prepended to the HOST and URI before
    //  looking in the rules for a match. If you configure CloudFront to
    //  forward http to https then this should always be the default.
    incomingProtocol: 'https://',  // default

    // 's3Options' is the options passed into the AWS SDK S3 client when instantiated.
    s3Options: { httpOptions: { connectTimeout: 2000 } },
  };

Origin-Request Event Object

The following is a typical event object that gets fed into your Lambda@Edge function for a origin-request event.

{
  "Records": [
    {
      "cf": {
        "request": {
          "clientIp": "99.99.999.999",
          "headers": {
            "host": [{
              "key": "Host",
                // 'value' will either be your [S3 bucket domain]
                // OR your incoming [Host] depending on if you whitelist it
              "value": "somedomain.com"
            }],
            "cloudFront-viewer-country": [{
              key:'CloudFront-Viewer-Country',
              value: "CA"
            }],
            "accept-language": [{
              key:'Accept-Language',
              value: "en-GB,en-US;q=0.9,fr-CA;q=0.7,en;q=0.8"
            }],
          },
          "method": "GET",
          "origin": {
            "s3": {
              "authMethod": "origin-access-identity",
              "customHeaders": {},
              // 'domainName' will be your [S3 bucket domain]
              "domainName": "some-unique-bucketname.s3.amazonaws.com",
              "path": "",
              "region": "us-east-1"
            }
          },
          "querystring": "",
          // 'uri' is your incoming request path
          // This is the property manipulated by middy-reroute
          // during a rewrite
          "uri": "/news"
        }
      }
    }
  ]
}

Future Features?

There are still a few things that Netlify's rules can do that middy-reroute can't. Additionally, there are quite a few new ones that come to mind that would be great.

Querystrings that are placeholder-able

# base case
/path/* /otherpath/:splat 301​

# one value or the other.  Must match exactly!
/path/* param1=:value1 /otherpath/:value1/:splat 301
/path/* param2=:value2 /otherpath/:value2/:splat 301​

# both values - ordering from the browser doesn't matter.
/path/* param1=:value1 param2=:value2 /otherpath/:value1/:value2/:splat 301

Access control dependent rules

This is an example of a feature that already exists using Netlify's rules. However, since this would require the middleware having a handle of you application's business logic, we'd need to think about how to best pass this into the middleware, to allow for customization. At the end of the day, the middleware is just going to parse the JWT token and match the important role bits to the rules. So we'd at least need to pass the middleware the role param path (in JWT) and the JWT secret or function for parsing the secret.

/admin/*	200!	Role=admin

Flattening chained rules

Another existing Netlify feature that I believe gets applied to all rules. Currently, middy-reroute just applies that first rule it finds in the rules file from top-to-bottom.

      /redirect1   /redirect2   302
      /redirect2   /redirect3   302

# =>  /redirect1   /redirect3   302`

Conditions that are placeholder-able

Thought this could be neat if there was a way to use the matched condition in the resolved URI somehow. Not sure what the proper indentifier would be though. And I could see you may want to define character case.

      /country_flag.gif   /flags/::country.gif     200
# =>  /country_flag.gif   /flags/ca.gif           200`

      /country_flag.gif   /flags/::COUNTRY.gif     200
# =>  /country_flag.gif   /flags/CA.gif           200`

Contributing

Everyone is very welcome to contribute to this repository. Feel free to raise issues or to submit Pull Requests.

License

Licensed under MIT License.