Deploy advanced HTTP traffic shaping with request variables and CEL

You’ve developed a read-only API and delivered it to production. Congratulations! But you notice that your upstream service is getting hammered by requests with POST, PUT, and DELETE methods, each of which returns a 501 Not Implemented error in response.

These requests aren’t harmful, but they also put an unnecessary load on your API service. Why not just block them at your first opportunity?

---
on_http_request:
  - name: "Reject all non-GET requests"
    expressions:
      - "req.method != 'GET'"
    actions:
      - type: "deny"
    	  config:
          status_code: 404

In this example of ngrok’s Traffic Policy module, you conditionally shape when and in what fashion HTTP traffic passes to and from your API—or device, service, database, and more, depending on what you’ve deployed. ngrok evaluates the rule against every request and denies any that evaluates to true (such as a request with the POST method) with a nondescript 404 response code.

You’ve successfully blocked the cruft from your API service entirely with just a few LOCs, a single expression written in Common Expression Language (CEL), and just one item—among dozens—of ngrok’s powerful new library of request variables.

What are these new request variables for?

Like connection variables, request variables are recent additions to Traffic Policy and open up novel use cases for ngrok that weren't possible before. By giving you access to details like headers, cookies, trailers, and the target URL, you can filter, take action on, and ultimately shape the way HTTP traffic hits your upstream service. Let's break down those opportunities, starting with some raw request data that passes through ngrok with every request.

GET /random HTTP/2
X-Forwarded-For: 174.73.243.140
X-Forwarded-Host: foo.example.com
X-Forwarded-Proto: https
Accept: */*
User-Agent: curl/8.7.1

The first line contains the essentials:

  • GET: The HTTP method, which defines the type of operation the user wishes to perform. With the req.method request variable, you can also see methods like POST to create new data or DELETE to remove a specified document.
  • /random: The path this HTTP request is attempting to access, which you can find in numerous variables like req.url.path or req.url.raw_path.
  • HTTP/2: The version of the HTTP protocol used in this request, represented in req.version.

The additional five lines are all optional headers that ngrok injects into the request before proxying it to an ngrok agent and the upstream service, app, or API. You can access these as a whole with req.cookies, or get values from specific cookies with req.cookies[k][i].value.

You can use request variables in two ways. First, you can include them in CEL expressions, like "req.method == 'POST' || req.method == 'PUT'", which evaluate to true/false whether or not a given request uses the POST or PUT method. Second, you can interpolate them into Traffic Policy actions, such as custom responses, adding headers, and logging, for additional flexibility and context-awareness. While you might assume request variables are only usable with rules in the inbound map, you can also interpolate request variables into your outbound rules, which apply to the associated response.

Based on ongoing conversations with users, we see a few request variables rise to the top:

  • req.content_type: The media type as specified in the Content-Type header, such as application/json.
  • req.method: The HTTP method associated with the request.
  • req.user_agent: An identifier of the application, OS, and vendor that’s attempting to access your endpoint.
  • req.url.uri: The full path of the requested URL plus any query strings.
  • req.cookies[k][i].value: The value of a cookie as specified by k, like sessionId.

As you can probably tell by now, we’ve organized them into the req namespace to help you find the appropriate request variables in our documentation and utilize them quickly.

One last reminder before you start shaping away: Unlike their connection-based siblings, request variables are only available with HTTP tunnels.

Use cases and examples of request variables in shaping HTTP traffic

Let’s dig into some examples to show you the many-faceted ways you can quickly implement request variables to unlock powerful HTTP traffic shaping strategies.

Protect your services from massive uploads

Any app or API that accepts user input must validate it before processing and storage, but in many cases, you can’t rely on client-side validation as a guarantee you’re not about to receive an unwanted payload. For example, your JavaScript app can apply some validation on file uploads on the client, but your user could also disable JavaScript and fire away. An API endpoint has even less upfront protection.

---
on_http_request:
  - name: "Block POST/PUT requests of excessive length"
    expressions:
      - "req.method == 'POST' || req.method == 'PUT'"
      - "req.content_length >= 10000000"
    actions:
      - type: "custom-response"
        config:
          status_code: 400
          content: "Error: The size of your upload exceeds our maximum of 10 megabytes."

If both expressions return true, in that a user tries to POST/PUT a request of more than 10 megabytes, ngrok will block the request from reaching your service and send back an informative error message.

Create ‘pretty’ URLs for better UX and SEO

Many content management systems use URLs with query strings and other implementation-level details to help instruct the application how to respond and with which data. For example, Wordpress’ plain permalink option creates URLs like https://example.com/blog/index.php?p=123&title=your-title, is rather unintuitive for the typical end user.

Instead, you should map these URLs using regular expressions and URL rewrites to “prettier” alternatives. You’ll make both your user and Google’s ranking algorithm happy, and you get the added bonus of hiding an important implementation detail from prying eyes.

---
on_http_request:
  - name: "Rewrite ugly URLs on the /blog/ path"
    expressions:
      - "req.url.path.startsWith('/blog')"
    actions:
      - type: "url-rewrite"
        config:
          from: "/blog/([0-9]+)/([a-zA-Z]+)/"
          to: "/blog/index.php?p=$1&title=$2"

This rule filters all requests using the req.url.path request variable, which applies to all “pretty” URLs, like https://example.com/blog/123/your-title/. The rule then uses regular expressions to capture the necessary information to build a new URL, including query strings, and perform the mapping via a rewrite. Your backend understands how to respond and the user doesn’t even know the URL changed in the first place.

Not sure whether a rewrite or redirect is the right option for your use case? We have a blog for that.

Deploy A/B tests

Update: This use case has become far more stable with two new CEL macros:—rand.double() and rand.int()—and the combination of internal and public endpoints.

The easiest way to create an A/B test is to have two upstream services running different versions. That allows you to direct traffic to service A or service B without having to manipulate your app/API directly to add some routing or other business logic.

When you have your two services running, use ngrok to create separate internal endpoints, like https://a.internal and https://b.internal.

On your public endpoint, apply the following Traffic Policy, which routes traffic to one or the other based on a randomly generated number between 0 and 1.

---
on_http_request: 
  - expressions:
      - "rand.double() >= 0.5"
    actions:
      - type: "forward-internal"
        config:
          url: https://b.internal
  - actions:
      - type: "forward-internal"
        config:
          url: https://a.internal

Hit your endpoint a few times and you'll see different results based on A vs. B!

And now, back to the previous "hacky" version:

Let’s say you’re preparing to release a new major version of your app or API and want to see how it performs against your current baseline. With Traffic Policy, you can set up a rudimentary A/B test that splits requests between the current site and testing deployment at example.com/v2/, while retaining all existing URLs.

---
on_http_request:
  - expressions:
      - "timestamp(req.ts.header_received).getMilliseconds() <= 500"
    actions:
      - type: "url-rewrite"
        config:
          from: "/?([.*]+)?"
          to: "/v2/$1"

The “magic” of this policy is in the use of the CEL macro timestamp(req.ts.header_received).getMilliseconds(), which returns the millisecond value at which ngrok receives the header of a given request. Because that value will be between 0 and 999, it evaluates based on whether the millisecond value is greater than or equal to 500, providing a rough degree of randomness. If you’d like to change the weighting between A and B, so that B receives only 30% of incoming requests, you can change the value to 300.

Deny requests without specific headers and return a custom Problem Details response

Finally, let’s look at how you can filter HTTP traffic based on the content of request headers and provide a better UX. While standard HTTP codes can inform users of why a particular request fails, you can take your error handling a step further by complying with the Problem Details for HTTP APIs spec, which helps you create helpful client error responses in cases where a simple 400 doesn’t tell the full story.

---
on_http_request:
  - expressions:
      - "!('api-version' in req.headers)"
    actions:
      - type: custom-response
      config:
        status_code: 400
        content: >
          {
            "type":"https://example.com/docs/errors/version/",
            "title":"Unspecified API version.",
            "detail":"You did not specify an API version in your request. The current available versions are `v2` and `v3`. To set the version, add the following header to your request: `api-version:v2` or `api-version-v3`.",
            "instance":"${req.url.path}"
          }
        headers:
          content-type: "application/json"

In this example, you check whether a user explicitly specifies the api-version header. If they fail to do so, then you return a helpful error response that points them to relevant documentation and specific steps they can take to fix the issue and get back to working with your API.

What shapes will you create with request variables?

To get started filtering and taking action on HTTP traffic with all these new request variables, sign up for a free ngrok account.

From there, check out the greatest hits compilation of resources on Traffic Policy as a whole:

If you can’t intuit exactly which request variables would be useful for you right away, let’s visualize request variables another way. When a request reaches your ngrok endpoint, even for the most basic of “tortoise facts” APIs, it contains far more than a target URL and client IP address—and ngrok even adds more context along the way. Your Traffic Inspector dashboard puts all this data into a nicely designed and human-readable format.

As you can imagine, once you understand what new request variables are available to you with every inbound and outbound connection, you can use it for far more interesting solutions than logs.

Thoughts or requests on how you’d like request variables and policies to work? Trying to write a particularly obscure policy and can’t quite get the CEL syntax right? Why not reserve your spot in the next edition of ngrok’s Office Hours? Join our DevEd and Product teams to demo common solutions, learn about endpoints and tunnels together, and answer your burning questions live.

Share this post
Joel Hans
Joel Hans is a Senior Developer Educator at ngrok. He has plenty of strong thoughts about documentation, developer education, developer marketing, and more.
Traffic Policy
API gateway
Features
Gateways
Production