rewrite_body

package module
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 25, 2026 License: Apache-2.0 Imports: 3 Imported by: 0

README

Rewrite Body

Main

A Traefik middleware plugin that rewrites HTTP response bodies with regular expressions — including bodies that arrive gzip or deflate encoded, which it decodes, rewrites and re-encodes on the way through.

Fork notice. This is a maintained fork of packruler/rewrite-body at v1.2.0, which is itself a fork of Traefik's plugin-rewritebody. Neither has taken a commit since November 2022. This one is kept working against current Traefik: v1.3.0 is verified against Traefik 3.7.9, compiled and under Yaegi. See CHANGELOG.md for what changed and NOTICE for every modification relative to upstream.

What it is good for

  • Repointing absolute URLs baked into content. A database restored from production is full of production hostnames; rewriting them in the response lets a staging or development environment serve that data as-is, instead of running search-and-replace over the database after every restore.
  • Injecting markup into pages you do not control. Adding a stylesheet or a script before </head> in an application you cannot patch.
  • Correcting a string an upstream hard-codes. A wrong link, a stale brand name, a scheme that should be https.

It works on any response body the plugin is allowed to buffer — see How it works for exactly which those are, and Operating notes for what it costs.

Requirements

Traefik v3, verified against 3.7.9. The plugin has no Go dependencies.

Installation

Plugins are declared in Traefik's static configuration. Load a tagged release:

experimental:
  plugins:
    rewrite-body:
      moduleName: "github.com/kelnei/rewrite-body"
      version: "v1.3.0"

Or run it from a working copy — mount the source at /plugins-local/src/github.com/kelnei/rewrite-body and declare it local. Traefik builds a local plugin at startup, so restart Traefik after changing the source:

experimental:
  localPlugins:
    rewrite-body:
      moduleName: "github.com/kelnei/rewrite-body"

Either way, middleware references in the dynamic configuration use the map name you chose (rewrite-body above), not the module path.

Configuration

Create a middleware in the dynamic configuration and attach it to a router. Every option below is shown with its default:

http:
  routers:
    my-router:
      rule: "Host(`localhost`)"
      service: my-service
      middlewares:
        - rewrite-foo

  middlewares:
    rewrite-foo:
      plugin:
        rewrite-body:
          # Applied in order: each pattern sees the output of the one before it.
          rewrites:
            - regex: "foo"
              replacement: "bar"

          # Keep the upstream Last-Modified on responses this plugin rewrites.
          # Defaults to false, which removes it, because a rewritten body is no
          # longer the one that header describes. A response the plugin forwards
          # untouched always keeps it.
          lastModified: false

          # Trace: -2, Debug: -1, Info: 0, Warning: 1, Error: 2.
          # Debug logs the per-request routing decisions. Trace additionally
          # dumps every response body before and after rewriting, so it is for
          # one-off debugging, not a busy route.
          logLevel: 0

          # Which responses are considered at all.
          monitoring:
            # Matched against the response Content-Type as a substring, so
            # text/html covers "text/html; charset=utf-8". Wildcards are not
            # supported here -- write the types out.
            types:
              - text/html
            # Matched exactly, and always ALL CAPS. HEAD is never processed
            # whatever this says: its response has no body to rewrite.
            methods:
              - GET
Option Type Default Notes
rewrites[].regex string RE2 syntax, as in Go's regexp. Applied to the whole body, so a match may span the writes the backend made.
rewrites[].replacement string $1 and ${name} expand to submatches; use $$ for a literal $.
lastModified bool false false removes Last-Modified from rewritten responses.
logLevel int8 0 (Info) -2 Trace, -1 Debug, 0 Info, 1 Warning, 2 Error.
monitoring.types []string ["text/html"] Substring match on the response Content-Type.
monitoring.methods []string ["GET"] Exact match. HEAD is always excluded.
Example: repoint production URLs at a staging host
http:
  middlewares:
    stage-urls:
      plugin:
        rewrite-body:
          monitoring:
            types:
              - text/html
              - application/json
          rewrites:
            # Absolute URLs, keeping the path.
            - regex: "https://www\\.example\\.com"
              replacement: "https://stage.example.com"
            # Protocol-relative and escaped forms as they appear in JSON.
            - regex: "https:\\\\/\\\\/www\\.example\\.com"
              replacement: "https:\\/\\/stage.example.com"
Example: inject a stylesheet
http:
  middlewares:
    inject-css:
      plugin:
        rewrite-body:
          rewrites:
            - regex: "</head>"
              replacement: '<link rel="stylesheet" href="/custom.css"></head>'

How it works

Which responses are processed

A response is rewritten only when all of the following hold. They are checked in order of cost, cheapest first:

  1. The request method is in monitoring.methods, and is not HEAD.
  2. The request is not a WebSocket upgrade.
  3. The request's Accept header does not rule out every monitored type. */*, a range such as text/*, and a missing header are all open-ended and pass this gate — Accept states what a client would like, not what the response will be, so it can only be used to skip requests that clearly want something else. This is why fetch and XMLHttpRequest responses are rewritten.
  4. The response Content-Type names one of monitoring.types. This is the check that actually decides.
  5. The response Content-Encoding is one of gzip, deflate, identity, or absent.
What happens to everything else

Anything failing 4 or 5 is forwarded to the client as the backend sent it — write by write, headers untouched, including the Content-Length and Last-Modified it arrived with. Nothing is buffered, so a download or an open-ended stream that happens to reach the plugin costs no memory here and is not held back.

Anything failing 1 to 3 never reaches the plugin's writer at all.

Encodings
Content-Encoding Handling
absent, identity Rewritten directly.
gzip Decoded with compress/gzip, rewritten, re-encoded.
deflate Decoded and re-encoded with compress/zlib, since HTTP's deflate means the zlib format of RFC 1950. A body arriving as bare RFC 1951 DEFLATE without the zlib wrapper is also decoded, because plenty of servers send it that way.
anything else, br included Forwarded untouched.

If a body cannot be decoded, or cannot be re-encoded after rewriting, the original response is sent instead. The rewrite is abandoned rather than the body truncated.

Headers

Content-Length is dropped from a rewritten response, because the rewrite changes the length; the final write establishes the new one. Forwarded responses keep theirs.

Operating notes

  • A rewritten response is buffered in full. A regular expression can match across the writes a backend makes, so the whole body has to be present before any pattern runs. Budget one body's worth of memory per in-flight rewritten request. To bound it, chain Traefik's Buffering middleware, which can cap a response size.
  • A monitored response that streams is buffered too, and so arrives at the client when it finishes rather than as it is produced. That is inherent to rewriting. Keep streaming endpoints — Server-Sent Events, long-poll, chunked progress — out of monitoring.types, and they pass straight through.
  • Traefik does not call the plugin's Flush. Yaegi hands compiled code a wrapper satisfying only http.ResponseWriter, so Traefik's check for an http.Flusher fails and it stops flushing for the route. A forwarded body still leaves as net/http's own response buffer fills, so a long stream keeps moving, but a stream under about 2 kB in total arrives at the end.
  • Aborted upstream responses log at ERROR. When a backend abandons a response, this plugin re-panics http.ErrAbortHandler so Traefik performs the abort rather than sending a 200 with no body. Yaegi wraps the panic value, which defeats Traefik's identity check on the sentinel, so its recovery middleware logs each abort at ERROR where an unplugged route logs DEBUG. Noise only.

Development

make          # lint and test
make test     # go test -v -cover ./...
make lint     # golangci-lint run
make yaegi_test

yaegi test resolves imports through GOPATH rather than the module cache, so the checkout has to sit at its own module path for that target to find the plugin's own packages:

mkdir -p "$GOPATH/src/github.com/kelnei"
ln -s "$PWD" "$GOPATH/src/github.com/kelnei/rewrite-body"

CI runs the whole set on every Go release upstream still supports, compiled and under Yaegi.

Traefik interprets this plugin, so its Go is not the Go you build with

Traefik does not compile plugins. It runs them through Yaegi, and Traefik 3.7.9 pins yaegi v0.16.1 — released April 2024, still the newest release, and lightly maintained since. Two consequences the compiler and go test will let you walk straight into:

The standard library is frozen at Go 1.22. Yaegi resolves stdlib names through symbol tables generated at its own release, and every table in v0.16.1 is go1_22_*. Anything added to the standard library after that does not exist at runtime, whatever go.mod says. httptest.NewRequestWithContext (Go 1.23) is a worked example: it compiles, go test passes, and then the interpreter rejects it.

package httptest "net/http/httptest" has no symbol NewRequestWithContext

Interfaces implemented here can be invisible to Traefik. Yaegi hands compiled code a wrapper satisfying the one interface it was asked for, so a type assertion inside Traefik for a second interface fails even though the method is right there. ResponseWrapper.Flush is the live case, described under Operating notes.

make yaegi_test catches the first of these and runs in CI on every push. The second shows up only against a running Traefik, so measure on the wire before trusting a change to the response path.

None of this is what the go directive in go.mod controls. That is the minimum toolchain, and it constrains the compiler and the linters; Yaegi caps what the code may actually use.

Licence and credits

Licensed under the Apache License, Version 2.0 — see LICENCE.

This product includes software developed by packruler, which is itself a fork of Traefik's plugin-rewritebody. NOTICE carries the attribution the licence requires, along with an itemised list of every modification made in this fork.

Documentation

Overview

Package rewrite_body a plugin to rewrite response body.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CreateConfig

func CreateConfig() *handler.Config

CreateConfig creates and initializes the plugin configuration.

func New

func New(context context.Context, next http.Handler, config *handler.Config, name string) (http.Handler, error)

New creates and returns a new rewrite body plugin instance.

Types

This section is empty.

Directories

Path Synopsis
Package compressutil a plugin to handle compression and decompression tasks
Package compressutil a plugin to handle compression and decompression tasks
Package handler a plugin to rewrite response body.
Package handler a plugin to rewrite response body.
Package httputil a package for handling http data tasks
Package httputil a package for handling http data tasks
Package logger a package for handling writing content to logs.
Package logger a package for handling writing content to logs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL