From 93c8a782fc456a939acfa5fe23df5f8898569c06 Mon Sep 17 00:00:00 2001
From: blessdyb
Date: Fri, 2 Oct 2026 02:35:03 -0700
Subject: [PATCH 1/2] Rewrite form bodies, not only JSON
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A request rule's body edit did nothing against a form POST. Reported with a "Set stockApi = http://localhost/admin"
rule that never fired: the request Chrome made was application/x-www-form-urlencoded —
stockApi=http%3A%2F%2Fstock.example.net%3A8080%2Fcheck
and the body edit was gated on the body parsing as a JSON object, so a form body fell through untouched. The
dialog said as much ("Only requests with a JSON body are changed"), which made it correct behaviour and a
useless feature for the most common kind of body there is.
So a body edit now also applies to a form body. When the body is not JSON and the request carries a
Content-Type of application/x-www-form-urlencoded, the path is a field name taken whole — a form is flat, so
`a.b` is a field literally named that, not a nested one — and the value is used as typed, because a form has
no types. Fields are parsed into ordered pairs that may repeat (set edits the first, remove drops all), and
re-encoded the way a browser does: + for space, everything else percent-encoded, so http://localhost/admin
goes on the wire as http%3A%2F%2Flocalhost%2Fadmin. Content-Length is reframed to match, as for JSON.
The Content-Type header is the signal: a body that merely looks like a form, with no such header, is still
left alone. JSON is tried first and is unchanged.
Co-Authored-By: Claude Opus 4.8
---
Flowlight/Inspection/RewriteRule.swift | 127 ++++++++++++++++++++++---
Flowlight/UI/RewriteRulesView.swift | 8 +-
FlowlightTests/RewriteRuleTests.swift | 77 +++++++++++++++
3 files changed, 193 insertions(+), 19 deletions(-)
diff --git a/Flowlight/Inspection/RewriteRule.swift b/Flowlight/Inspection/RewriteRule.swift
index e0ee605..054dda3 100644
--- a/Flowlight/Inspection/RewriteRule.swift
+++ b/Flowlight/Inspection/RewriteRule.swift
@@ -22,9 +22,10 @@ struct HeaderEdit: Codable, Equatable, Identifiable, Sendable {
}
}
-/// An edit to the request's JSON body, addressed by a dotted key path into objects (`metadata.user`). `set` creates
-/// the path if needed; `remove` deletes the leaf. The value is parsed as JSON when it can be (`0.7`, `true`,
-/// `{"a":1}`) and taken as a plain string otherwise.
+/// An edit to the request's body. For a JSON body, `path` is a dotted key path into objects (`metadata.user`):
+/// `set` creates the path if needed, `remove` deletes the leaf, and the value is parsed as JSON when it can be
+/// (`0.7`, `true`, `{"a":1}`) and taken as a plain string otherwise. For a form body
+/// (`application/x-www-form-urlencoded`), `path` is a field name taken whole and the value is used as typed.
struct BodyEdit: Codable, Equatable, Identifiable, Sendable {
enum Op: String, Codable, Sendable, CaseIterable { case set, remove }
var id = UUID()
@@ -46,8 +47,7 @@ struct BodyEdit: Codable, Equatable, Identifiable, Sendable {
}
}
-/// A rule that rewrites a matching outgoing request before it is forwarded upstream — changing headers or the JSON
-/// body. Like a mock, but it edits the request and lets it through rather than answering it: add an `Authorization`
+/// A rule that rewrites a matching outgoing request before it is forwarded upstream — changing headers or the body (JSON or form). Like a mock, but it edits the request and lets it through rather than answering it: add an `Authorization`
/// header, pin `model`, strip a tracking field. Matched like a mock (host / path glob / method), and applied by the
/// same intervention path the guardrails use. Only requests Flowlight decrypts and can buffer (bodies up to a few MB,
/// not chunked or streamed) can be rewritten.
@@ -139,18 +139,35 @@ enum RewriteRules {
}
let bodyEdits = applicable.flatMap(\.body)
- if !bodyEdits.isEmpty, !bodyData.isEmpty,
- var json = (try? JSONSerialization.jsonObject(with: bodyData)) as? [String: Any] {
- var changed = false
- for edit in bodyEdits {
- let comps = edit.path.split(separator: ".").map(String.init)
- guard !comps.isEmpty else { continue }
- switch edit.op {
- case .set: setJSON(&json, path: comps, value: parseValue(edit.value)); changed = true; notes.append("set \(edit.path)")
- case .remove: removeJSON(&json, path: comps); changed = true; notes.append("removed \(edit.path)")
+ if !bodyEdits.isEmpty, !bodyData.isEmpty {
+ if var json = (try? JSONSerialization.jsonObject(with: bodyData)) as? [String: Any] {
+ var changed = false
+ for edit in bodyEdits {
+ let comps = edit.path.split(separator: ".").map(String.init)
+ guard !comps.isEmpty else { continue }
+ switch edit.op {
+ case .set: setJSON(&json, path: comps, value: parseValue(edit.value)); changed = true; notes.append("set \(edit.path)")
+ case .remove: removeJSON(&json, path: comps); changed = true; notes.append("removed \(edit.path)")
+ }
}
+ if changed, let out = try? JSONSerialization.data(withJSONObject: json) { bodyData = out }
+ } else if isFormEncoded(headerLines), var form = FormBody(bodyData) {
+ // A form body is flat, so a path is a field name taken whole — `a.b` means a field literally
+ // named `a.b`, not a nested one. The value is always text; forms have no types, so it is used
+ // verbatim rather than through `parseValue`.
+ var changed = false
+ for edit in bodyEdits {
+ let field = edit.path
+ guard !field.isEmpty else { continue }
+ switch edit.op {
+ case .set:
+ form.set(field, to: edit.value); changed = true; notes.append("set \(field)")
+ case .remove:
+ if form.remove(field) { changed = true; notes.append("removed \(field)") }
+ }
+ }
+ if changed { bodyData = form.encoded() }
}
- if changed, let out = try? JSONSerialization.data(withJSONObject: json) { bodyData = out }
}
guard !notes.isEmpty else { return nil }
@@ -174,6 +191,19 @@ enum RewriteRules {
return s
}
+ /// Whether the request carries an `application/x-www-form-urlencoded` body, from its Content-Type header.
+ ///
+ /// A `charset` or other parameter may follow (`...; charset=utf-8`), so this matches the media type as a
+ /// prefix rather than the whole value.
+ static func isFormEncoded(_ headerLines: [String]) -> Bool {
+ for line in headerLines where line.lowercased().hasPrefix("content-type:") {
+ let value = line.drop(while: { $0 != ":" }).dropFirst()
+ .trimmingCharacters(in: .whitespaces).lowercased()
+ return value.hasPrefix("application/x-www-form-urlencoded")
+ }
+ return false
+ }
+
private static func setJSON(_ object: inout [String: Any], path: [String], value: Any) {
guard let key = path.first else { return }
if path.count == 1 { object[key] = value; return }
@@ -190,3 +220,70 @@ enum RewriteRules {
object[key] = child
}
}
+
+/// An `application/x-www-form-urlencoded` body as its ordered `name=value` pairs.
+///
+/// Ordered rather than a dictionary, and able to hold the same name twice, because a form can — and a rewrite
+/// that silently collapsed `tag=a&tag=b` into one field would change a request in a way nobody asked for. Set
+/// edits the first pair of that name in place (or appends when there is none); remove drops every pair of that
+/// name. Decoding and re-encoding follow the form rules: `+` is a space, everything else is percent-encoded.
+struct FormBody {
+ private var pairs: [(name: String, value: String)]
+
+ /// Parses a form body. Fails only if the bytes are not UTF-8; an empty or shapeless body parses to no pairs,
+ /// which is a body a `set` can still add a field to.
+ init?(_ data: Data) {
+ guard let text = String(data: data, encoding: .utf8) else { return nil }
+ pairs = []
+ guard !text.isEmpty else { return }
+ for part in text.components(separatedBy: "&") where !part.isEmpty {
+ if let eq = part.firstIndex(of: "=") {
+ let name = Self.decode(String(part[part.startIndex.. Bool {
+ let before = pairs.count
+ pairs.removeAll { $0.name == name }
+ return pairs.count != before
+ }
+
+ /// Re-encodes the pairs. Field order is preserved so a diff reads as the one change that was made.
+ func encoded() -> Data {
+ let body = pairs.map { "\(Self.encode($0.name))=\(Self.encode($0.value))" }.joined(separator: "&")
+ return Data(body.utf8)
+ }
+
+ /// Form-decode one component: `+` is a space, then `%XX` is a byte. A malformed `%` is left as written
+ /// rather than dropped, so a value is never silently truncated.
+ private static func decode(_ s: String) -> String {
+ s.replacingOccurrences(of: "+", with: " ").removingPercentEncoding
+ ?? s.replacingOccurrences(of: "+", with: " ")
+ }
+
+ /// Form-encode one component. Everything outside the unreserved set is percent-encoded and space becomes
+ /// `+`, which is what a browser emits — so `http://localhost/admin` becomes `http%3A%2F%2Flocalhost%2Fadmin`.
+ private static func encode(_ s: String) -> String {
+ let unreserved = CharacterSet(charactersIn:
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789*-._ ")
+ let percent = s.addingPercentEncoding(withAllowedCharacters: unreserved) ?? s
+ return percent.replacingOccurrences(of: " ", with: "+")
+ }
+}
diff --git a/Flowlight/UI/RewriteRulesView.swift b/Flowlight/UI/RewriteRulesView.swift
index f3a7f02..4a11a43 100644
--- a/Flowlight/UI/RewriteRulesView.swift
+++ b/Flowlight/UI/RewriteRulesView.swift
@@ -1,6 +1,6 @@
import SwiftUI
-/// Modify requests: rules that edit an outgoing request's headers or JSON body before it's forwarded. Sits with the
+/// Modify requests: rules that edit an outgoing request's headers or body (JSON or form) before it's forwarded. Sits with the
/// rest of inspection setup — a rule can only change a request Flowlight decrypts.
struct RewriteRulesSection: View {
@ObservedObject var inspection: InspectionController
@@ -9,7 +9,7 @@ struct RewriteRulesSection: View {
var body: some View {
VStack(alignment: .leading, spacing: 10) {
- Text(L("Change a request on its way out — add or replace a header, pin a field in the JSON body, or strip one — and let it continue to the server. Like a mock, but it edits the request instead of answering it."))
+ Text(L("Change a request on its way out — add or replace a header, pin a field in the JSON or form body, or strip one — and let it continue to the server. Like a mock, but it edits the request instead of answering it."))
.font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
Label(L("Only requests Flowlight decrypts can be rewritten, and only buffered ones — bodies up to a few megabytes. Tunnelled, pinned, chunked or streamed uploads pass through untouched."),
systemImage: "info.circle")
@@ -155,7 +155,7 @@ struct RewriteRuleEditor: View {
// Body edits
VStack(alignment: .leading, spacing: 6) {
- Text(L("JSON body")).font(.caption.bold()).foregroundStyle(.secondary)
+ Text(L("Request body")).font(.caption.bold()).foregroundStyle(.secondary)
ForEach($rule.body) { $edit in
HStack(spacing: 6) {
Picker("", selection: $edit.op) {
@@ -171,7 +171,7 @@ struct RewriteRuleEditor: View {
}
}
Button(L("Add body edit")) { rule.body.append(BodyEdit()) }.controlSize(.small)
- Text(L("Dotted path into the JSON object (metadata.user). A value that is valid JSON (0.7, true, {\"a\":1}) is used as-is; anything else is a string. Only requests with a JSON body are changed."))
+ Text(L("For a JSON body, a dotted path into the object (metadata.user); a value that is valid JSON (0.7, true, {\"a\":1}) is used as-is, anything else is a string. For a form body (application/x-www-form-urlencoded), the field name, taken whole, set to the text as typed. Only requests with one of those two bodies are changed."))
.font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
}
diff --git a/FlowlightTests/RewriteRuleTests.swift b/FlowlightTests/RewriteRuleTests.swift
index bf28c02..0e869a5 100644
--- a/FlowlightTests/RewriteRuleTests.swift
+++ b/FlowlightTests/RewriteRuleTests.swift
@@ -103,6 +103,83 @@ final class RewriteRuleTests: XCTestCase {
XCTAssertTrue(parts(out).head.contains("X-Tag: 1"))
}
+ // MARK: Form bodies
+
+ private let form = "Content-Type: application/x-www-form-urlencoded"
+
+ private func bodyForm(_ data: Data) -> [String: String] {
+ guard let sep = data.range(of: Data("\r\n\r\n".utf8)) else { return [:] }
+ let body = String(decoding: data[sep.upperBound...], as: UTF8.self)
+ var out: [String: String] = [:]
+ for part in body.components(separatedBy: "&") where !part.isEmpty {
+ let halves = part.components(separatedBy: "=")
+ let name = (halves.first ?? "").removingPercentEncoding ?? ""
+ let value = halves.dropFirst().joined(separator: "=")
+ .replacingOccurrences(of: "+", with: " ").removingPercentEncoding ?? ""
+ out[name] = value
+ }
+ return out
+ }
+
+ /// The reported case: a form field set to a URL, the value percent-encoded, Content-Length reframed. This is
+ /// what the "Set stockApi = http://localhost/admin" rule did nothing for, because the body is a form, not JSON.
+ func testFormSetEncodesValue() throws {
+ let rule = RewriteRule(host: "api.example.com", body: [BodyEdit(op: .set, path: "stockApi", value: "http://localhost/admin")])
+ let out = try XCTUnwrap(apply(rule, request("POST", "/product/stock", headers: [form],
+ body: "stockApi=http%3A%2F%2Fstock.example.net%3A8080%2Fcheck"),
+ path: "/product/stock"))
+ XCTAssertEqual(bodyForm(out)["stockApi"], "http://localhost/admin")
+ // The value is percent-encoded on the wire, and Content-Length agrees with the final body.
+ XCTAssertTrue(parts(out).body.contains("http%3A%2F%2Flocalhost%2Fadmin"))
+ let declared = parts(out).head.first { $0.lowercased().hasPrefix("content-length:") }?
+ .components(separatedBy: ": ").last.flatMap { Int($0) }
+ let sep = try XCTUnwrap(out.range(of: Data("\r\n\r\n".utf8)))
+ XCTAssertEqual(declared, out.distance(from: sep.upperBound, to: out.endIndex))
+ }
+
+ /// Set adds a field that was not there, leaving the others in place and in order.
+ func testFormSetAppendsNewField() throws {
+ let rule = RewriteRule(host: "api.example.com", body: [BodyEdit(op: .set, path: "role", value: "admin")])
+ let out = try XCTUnwrap(apply(rule, request("POST", "/login", headers: [form], body: "user=alice&pass=x"), path: "/login"))
+ XCTAssertEqual(bodyForm(out)["user"], "alice")
+ XCTAssertEqual(bodyForm(out)["pass"], "x")
+ XCTAssertEqual(bodyForm(out)["role"], "admin")
+ }
+
+ /// Remove drops a field; removing one that is not there is no change.
+ func testFormRemove() throws {
+ let rule = RewriteRule(host: "api.example.com", body: [BodyEdit(op: .remove, path: "csrf")])
+ let out = try XCTUnwrap(apply(rule, request("POST", "/login", headers: [form], body: "user=alice&csrf=tok"), path: "/login"))
+ XCTAssertNil(bodyForm(out)["csrf"])
+ XCTAssertEqual(bodyForm(out)["user"], "alice")
+
+ let miss = RewriteRule(host: "api.example.com", body: [BodyEdit(op: .remove, path: "nope")])
+ XCTAssertNil(apply(miss, request("POST", "/login", headers: [form], body: "user=alice"), path: "/login"))
+ }
+
+ /// Set replaces the first pair of a repeated field and leaves the rest, rather than collapsing them.
+ func testFormSetReplacesInPlaceKeepingOrder() throws {
+ let rule = RewriteRule(host: "api.example.com", body: [BodyEdit(op: .set, path: "tag", value: "z")])
+ let out = try XCTUnwrap(apply(rule, request("POST", "/x", headers: [form], body: "tag=a&tag=b&keep=1"), path: "/x"))
+ let body = String(decoding: out[(out.range(of: Data("\r\n\r\n".utf8))!.upperBound)...], as: UTF8.self)
+ XCTAssertEqual(body, "tag=z&tag=b&keep=1")
+ }
+
+ /// Without the form Content-Type, a body shaped like a form is still left untouched — the header is the signal.
+ func testFormBodyWithoutContentTypeIsNotTouched() {
+ let rule = RewriteRule(host: "api.example.com", body: [BodyEdit(op: .set, path: "a", value: "2")])
+ XCTAssertNil(apply(rule, request("POST", "/x", body: "a=1&b=2"), path: "/x"))
+ }
+
+ /// A charset parameter after the media type does not stop it being recognised as a form.
+ func testFormContentTypeWithCharset() throws {
+ let rule = RewriteRule(host: "api.example.com", body: [BodyEdit(op: .set, path: "a", value: "2")])
+ let out = try XCTUnwrap(apply(rule, request("POST", "/x",
+ headers: ["Content-Type: application/x-www-form-urlencoded; charset=utf-8"],
+ body: "a=1"), path: "/x"))
+ XCTAssertEqual(bodyForm(out)["a"], "2")
+ }
+
// MARK: Matching
/// A rule for another host, method or path doesn't touch the request.
From bbecd5c7d80db74280f7316578f9d4122ee61b84 Mon Sep 17 00:00:00 2001
From: blessdyb
Date: Sat, 3 Oct 2026 21:30:06 -0700
Subject: [PATCH 2/2] Flowlight 0.13.1
Rewrite rules now support application/x-www-form-urlencoded request bodies,
so form fields can be set or removed before the request is forwarded. Preserve
repeated fields and field order, re-encode values like a browser, and reframe
Content-Length. Add form-body coverage and release metadata for 0.13.1.
Co-Authored-By: Claude Code
---
docs/404.html | 2 +-
docs/about/index.html | 2 +-
docs/de/about/index.html | 2 +-
docs/de/docs/index.html | 4 ++--
docs/de/index.html | 6 +++---
docs/de/privacy/index.html | 2 +-
docs/de/threat-model/index.html | 2 +-
docs/docs/index.html | 4 ++--
docs/es/about/index.html | 2 +-
docs/es/docs/index.html | 4 ++--
docs/es/index.html | 6 +++---
docs/es/privacy/index.html | 2 +-
docs/es/threat-model/index.html | 2 +-
docs/fr/about/index.html | 2 +-
docs/fr/docs/index.html | 4 ++--
docs/fr/index.html | 6 +++---
docs/fr/privacy/index.html | 2 +-
docs/fr/threat-model/index.html | 2 +-
docs/index.html | 6 +++---
docs/it/about/index.html | 2 +-
docs/it/docs/index.html | 4 ++--
docs/it/index.html | 6 +++---
docs/it/privacy/index.html | 2 +-
docs/it/threat-model/index.html | 2 +-
docs/ja/about/index.html | 2 +-
docs/ja/docs/index.html | 4 ++--
docs/ja/index.html | 6 +++---
docs/ja/privacy/index.html | 2 +-
docs/ja/threat-model/index.html | 2 +-
docs/ko/about/index.html | 2 +-
docs/ko/docs/index.html | 4 ++--
docs/ko/index.html | 6 +++---
docs/ko/privacy/index.html | 2 +-
docs/ko/threat-model/index.html | 2 +-
docs/llms-full.txt | 14 +++++++++++---
docs/llms.txt | 2 +-
docs/privacy/index.html | 2 +-
docs/pt-PT/about/index.html | 2 +-
docs/pt-PT/docs/index.html | 4 ++--
docs/pt-PT/index.html | 6 +++---
docs/pt-PT/privacy/index.html | 2 +-
docs/pt-PT/threat-model/index.html | 2 +-
docs/releases/index.html | 14 ++++++++++++--
docs/sitemap.xml | 2 +-
docs/threat-model/index.html | 2 +-
docs/zh-Hans/about/index.html | 2 +-
docs/zh-Hans/docs/index.html | 4 ++--
docs/zh-Hans/index.html | 6 +++---
docs/zh-Hans/privacy/index.html | 2 +-
docs/zh-Hans/threat-model/index.html | 2 +-
docs/zh-Hant/about/index.html | 2 +-
docs/zh-Hant/docs/index.html | 4 ++--
docs/zh-Hant/index.html | 6 +++---
docs/zh-Hant/privacy/index.html | 2 +-
docs/zh-Hant/threat-model/index.html | 2 +-
project.yml | 2 +-
site/pages/releases.html | 12 +++++++++++-
57 files changed, 118 insertions(+), 90 deletions(-)
diff --git a/docs/404.html b/docs/404.html
index f871b1f..fa38be0 100644
--- a/docs/404.html
+++ b/docs/404.html
@@ -80,7 +80,7 @@
diff --git a/docs/llms-full.txt b/docs/llms-full.txt
index c5a5d29..db7c9b6 100644
--- a/docs/llms-full.txt
+++ b/docs/llms-full.txt
@@ -62,7 +62,7 @@ Documentation
Using Flowlight
- Everything from the first launch to tuning the agent rules. Flowlight 0.13.0, macOS 15 or later.
+ Everything from the first launch to tuning the agent rules. Flowlight 0.13.1, macOS 15 or later.
On this page
@@ -839,7 +839,7 @@ Understand your AI agents.
brew install --cask xinbetween/tap/flowlightCopy
- v0.13.0macOS 15+UniversalGPL-3.0No telemetry
+ v0.13.1macOS 15+UniversalGPL-3.0No telemetry
connectionslive
@@ -1398,8 +1398,16 @@ Releases
Downloads, checksums and full notes for each version are on GitHub Releases.
+ 0.13.1
+October 3, 2026Latest
+
+ Rewrite form requests too. Request-modification rules now change fields in
+ application/x-www-form-urlencoded bodies, not only JSON. Set a field to a URL, text or
+ value and Flowlight encodes it as a browser would; remove a field and it is omitted. The request's
+ Content-Length is recalculated before it goes upstream.
+
0.13.0
-October 1, 2026Latest
+October 1, 2026
Change a request on its way out. A new kind of rule edits a matching outgoing
request before it reaches the server — add or replace a header, pin a field in the JSON body, or strip
diff --git a/docs/llms.txt b/docs/llms.txt
index 106f436..b5b8780 100644
--- a/docs/llms.txt
+++ b/docs/llms.txt
@@ -1,6 +1,6 @@
# Flowlight
-> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.13.0.
+> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.13.1.
- [Download Flowlight.dmg](https://github.com/xinbetween/flowlight/releases/latest/download/Flowlight.dmg)
- [Source code](https://github.com/xinbetween/flowlight)
diff --git a/docs/privacy/index.html b/docs/privacy/index.html
index d057e20..3b7b925 100644
--- a/docs/privacy/index.html
+++ b/docs/privacy/index.html
@@ -174,7 +174,7 @@
Rewrite form requests too. Request-modification rules now change fields in
+ application/x-www-form-urlencoded bodies, not only JSON. Set a field to a URL, text or
+ value and Flowlight encodes it as a browser would; remove a field and it is omitted. The request's
+ Content-Length is recalculated before it goes upstream.
+
+
+
+
+
0.13.0
Change a request on its way out. A new kind of rule edits a matching outgoing
request before it reaches the server — add or replace a header, pin a field in the JSON body, or strip
@@ -1026,7 +1036,7 @@
Rewrite form requests too. Request-modification rules now change fields in
+ application/x-www-form-urlencoded bodies, not only JSON. Set a field to a URL, text or
+ value and Flowlight encodes it as a browser would; remove a field and it is omitted. The request's
+ Content-Length is recalculated before it goes upstream.
+
+
+
+
+
0.13.0
Change a request on its way out. A new kind of rule edits a matching outgoing
request before it reaches the server — add or replace a header, pin a field in the JSON body, or strip