Drop-in API gateway policy examples for manipulating headers

October 17, 2024
|
8
min read
Joel Hans

After the success of my last drop-in example gallery (welcome! to all those who signed up after reading it), I thought the time was right for another suite of Traffic Policy examples. Only this time, we’re focused on a single subject:

What can you accomplish by thoughtfully adding, editing, and removing headers from the requests and responses that pass through your API gateway?

Let’s look at a handful of use cases for why you would need to manipulate headers, then give you the exact Traffic Policy rule to get you started adopting it to your deployments and networks.

Setting the stage for header manipulation

If you’re not yet familiar with Traffic Policy and how it simplifies the process of creating and applying policies to your API gateway, I highly encourage you to read up on the section in that gallery blog titled How ngrok lets you quickly add API policy and traffic management and the launch announcement: Introducing ngrok’s Traffic Policy module.

Those resources will teach you more about how the Traffic Policy module works and why we built it, but in short:

Traffic Policy provides a flexible, programmable, and uniform approach acting conditionally on requests and responses across all the ways you use ngrok.

To use the drop-in Traffic Policy rules with the ngrok agent, you can copy-paste the YAML content into a file—let’s name it header-magic.yaml for fun!—and then reference that file when you start a new tunnel:

ngrok http {SERVICE_PORT} --traffic-policy-file=header-magic.yaml


When the ngrok agent starts up, it pipes your Traffic Policy rule to our network, applying all the evaluation, logic, and actions for you.

When you step a little deeper into Traffic Policy documentation for HTTP tunnels, you’ll notice two mentions of headers: add-headers and remove-headers. You can use these actions together to add net-new headers, replace existing ones (by first removing them and adding their replacements), or simply delete unwanted metadata.

But now you might be asking: What can (or should) I do with headers?

Example #1: Change host headers to match upstream address and avoid errors

One common issue folks face when insert ngrok between their upstream services and the public internet is that when requests pass through ngrok, they're given a Host header like Host: foo.ngrok.dev. The Host header specifies the hostname/port of the upstream service being requested, and if your server configured to process a host like foo.ngrok.dev, it'll return an error.

In some cases, you actually can't reconfigure your server or upstream service. Even if you can, it's often easier to use ngrok to rewrite the Host header and requests flowing again.

---
on_http_request:
  - name: "Update the host header"
    expressions: []
    actions:
      - type: add-headers
        config:
          headers:
            host: {YOUR_HOST_HERE}

Example #2: Remove service details from response headers

Depending on how you’ve configured your upstream services—or often completely unbeknownst to you—they add response headers that you might consider as revealing too much information about your internal infrastructure.

For example, Express always adds an X-Powered-By: Express header to responses, and the ngrok network adds other details, like the entity tag (Etag) and IP address of the ngrok agent. You can use the remove headers action to decide exactly what does—and doesn’t—reach your end users.

---
on_http_response:
  - name: "Remove service details from response headers"
    expressions: []
    actions:
      - type: remove-headers
        config:
          headers:
            - "X-Powered-By"
            - "Ngrok-Agent-Ips"
            - "x-detail-foo"
            - "x-detail-bar"

Example #3: Add tracing/logging details

If you’re actively trying to debug an issue with your app or API, the more detail, the better. Adding headers from your gateway is one easy way to inject some crucial information about the request-response lifecycle into your upstream service.

To enrich your “headerspace,” you can add any available variables—minus the response-specific ones, as they’re unavailable until the outbound phase.

---
on_http_request:
  - name: "Add tracing/logging details"
    expressions: []
    actions:
      - type: add-headers
        config:
          headers:
            x-endpoint-id: "${endpoint.id}"
            x-client-ip: "${conn.client_ip}"
            x-client-conn-start: "${conn.ts.start}"
            x-client-loc: "${conn.geo.city}, ${conn.geo.country}"
            x-client-path: "${req.url.path}

This adds a plethora of header information available inside your network, which you can log for further analysis. You might even add logic into your service to respond conditionally based on which headers are present or their values.

GET /hello HTTP/2
X-Client-Ip: 🤫
X-Client-Loc: Tucson, United States
X-Client-Path: /hello
User-Agent: curl/8.7.1
X-Client-Route: /hello
Accept: */*
X-Endpoint-Id: ep_2nXCmH3pkBru5tlxcfSKWnQd8kK
X-Forwarded-Proto: https
X-Client-Conn-Start: 2024-10-16T20:23:11Z
X-Forwarded-For: 🤫
X-Forwarded-Host: header-magic.joelhans.xyz

Of course, you could also just save yourself the hassle and use our Traffic Events system and pipe all sorts of events to your event destination of choice… but we’re all about giving you the flexibility to make ngrok work with what you already have.

Example #4: Streamline headers to control logging compute and costs

Conversely, you may feel like your headspace is too rich with information, bogging down your homegrown logging system and costing you more on compute than you’d like.

You can just as easily strip away headers on incoming requests—just be aware that removing some of these may affect your upstream service operations.

---
on_http_request:
  - expressions: []
    actions:
      - type: "remove-headers"
        config:
          headers:
            - "X-Forwarded-For"
            - "X-Forwarded-Host"
            - "X-Forwarded-Proto"
            - "User-Agent"
            - "x-custom-header"

Example #5: Add SLA rules and report on metrics

If you give a customer a mission-critical API, chances are they’ll want an SLA to go with it.

By combining a few macros and expressions and throwing a bit of variable interpolation into the mix, you can create rules that help you provide a better service and give your customers more details about the service they’re paying for.

The below rule adds two relevant headers:

  • On the request, ngrok adds a x-sla-tier: platinum header for any request coming from a known IP address of your customer(s) using the inCidrRanges macro. You could also test for the existence of a specific cookie, or add this header only based on whether or not the request is authenticated.
  • On the response, we use CEL interpolation to roughly calculate the duration of the request and response by calculating the difference between now and when the connection began.
---
on_http_request:
  - expressions:
      - "inCidrRanges(conn.client_ip, ['66.249.66.1/24', '2001:4860::/32'])"
    name: "Add headers for SLA clients"
    actions:
      - type: add-headers
        config:
          headers:
            x-sla-tier: "platinum"
on_http_response:
  - name: "Add SLA data"
    actions:
      - type: add-headers
        config:
          headers:
            x-sla-duration: "${timestamp(conn.ts.start) - timestamp(time.now)}"

As with the last example, you’ll get more accurate data with our eventing system, but these strategies absolutely do work in a pinch to give your finicky customers some details about how their interactions with your service are performing.

You can pair these SLA headers with rate limiting for even more control over the end-user experience.

Example #6: Create geo-aware services

I included a different strategy for this in a previous post, but multiple ways of accomplishing a similar goal is never a bad thing. Instead of rewriting URLs based on country codes, why not just add a header and let your upstream service do the rest of the heavy lifting, like triggering a localization service based on the values of x-to-localize and x-country-code?

---
on_http_request:
  - expressions:
      - "conn.geo.country_code in ['MX', 'JP', 'IN']"
    actions:
      - type: "add-header"
        config:
          headers:
            x-to-localize: true
            x-country-code: "${conn.geo.country_code}"

Example #7: Take further action on headers you’ve added

The more you tweak headers at the API gateway level, the more likely you’ll run into a situation where you want to take immediate action based on what you’ve added, edited, or removed, not leaving everything to your upstream service.

For example, you could extend the SLA example from above to create a rate limiting action that only applies to requests where you’ve previously added the x-sla-tier header. Since you've already conditionally applied the platinum header to specific IPs, you can thus rate limit them individually while still achieving limit fairness across all usage of your API.

This uses action result variables, which make new variables available for use in subsequent expressions and CEL interpolations. Essentially, you can chain together conditional actions to keep all this logic and processing at your API gateway, not built into your upstream service.

---
on_http_request:
  ...
  - expressions:
      - "actions.ngrok.add_headers.headers_added['x-sla-tier'] == 'platinum'"
    actions:
      - type: "rate-limit"
        config:
          name: "Allow platinum-sized API usage"
          algorithm: "sliding_window"
          capacity: 10000
          rate: "60m"
          bucket_key:
            - "conn.client_ip"

Time for your own header magic!

Ready to try these header-tweaking rules for yourself? Start by signing up for a free ngrok account.

If you’re looking for a testbed where experiment freely, why not check out our ngrok-samples repo on GitHub? Our DevRel and customer success teams are always adding new demos and sample apps for you to experiment with how ngrok works, from Traffic Policy and beyond.

For example, I just pushed a new API service that just “pongs” back information about your request—not very feature-rich, but perfect for testing. You can set it up like so:

git clone git@github.com:ngrok-samples/ngrok-api-service.git
cd ngrok-api-service
npm install
npm start

Start the ngrok agent with an HTTP tunnel to port 4000, referencing a Traffic Policy file you’ve saved to your local workstation.

ngrok http 4000 --traffic-policy-file=header-magic.yaml

From there, experiment with abandon! The agent will let you know if you have syntax errors in your Traffic Policy rule, and you have nothing to worry about with repeatedly stopping the agent, editing some YAML, and starting up the agent again. I certainly did that quite a few times in writing this.

I’d love to see what kind of header magic you’ve created! Let us know your experiences and experiments on the ngrok community repo.

If you’re feeling a little uncertain about how you could use Traffic Policy, consider registering for Office Hours? We’d love to run a Traffic Policy-specific session, taking 45 minutes or so to chat about all things expressions and actions—but we need your questions first!

Share this post
Joel Hans
Joel Hans is a Senior Developer Educator. Away from blog posts and demo apps, you might find him mountain biking, writing fiction, or digging holes in his yard.
API gateway
Traffic Policy
Gateways
Production