HttpApiClient: `Redacted` query and header values appear in plain text in client spans
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 30/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Stale
- Tech stack
- typescript
- Domain
- api, observability
Research direction
Start by reading HttpApiClient.ts (L552–L562) where query/header schemas are encoded, then tracing.ts (L40–L52) and HttpClient.ts (L731–L739) for name-based redaction. Implement per-endpoint collection of Redacted fields to add to CurrentRedactedNames during request execution. Done means the repro prints no s3cret in url.full, url.query, or http.request.header.x-token.
Written by the indexing model from the issue text.
Description
Follow-up to #9838. #9855 fixed HttpClientError, so an error no longer prints the request. Client spans have the same cause and still record the secrets: a Redacted query value appears in plain text in url.full and url.query, and a Redacted header appears in http.request.header.* once header capture is on.
import { Effect, Layer, Redacted, Schema, Tracer } from "effect"
import { HttpClient, HttpClientResponse } from "effect/http"
import { HttpApi, HttpApiClient, HttpApiEndpoint, HttpApiGroup } from "effect/http-api"
const Api = HttpApi.make("api").add(
HttpApiGroup.make("g").add(
HttpApiEndpoint.post("join", "/join", {
query: { token: Schema.RedactedFromValue(Schema.String) },
headers: { "x-token": Schema.RedactedFromValue(Schema.String) }
})
)
)
// a client whose every request succeeds
const ok = Layer.succeed(
HttpClient.HttpClient,
HttpClient.make((request) => Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status: 204 }))))
)
// a tracer that records span attributes
const attributes: Array<[string, unknown]> = []
const tracer = Tracer.make({
span: (options) => {
const span = new Tracer.NativeSpan(options)
const attribute = span.attribute.bind(span)
span.attribute = (key, value) => {
attributes.push([key, value])
attribute(key, value)
}
return span
}
})
const program = Effect.gen(function*() {
const client = yield* HttpApiClient.make(Api, { baseUrl: "http://x" })
const secret = Redacted.make("s3cret")
yield* client.g.join({ query: { token: secret }, headers: { "x-token": secret } })
for (const [key, value] of attributes) if (String(value).includes("s3cret")) console.log(key, value)
})
Effect.runPromise(
program.pipe(
Effect.provide(ok),
Effect.withTracer(tracer),
// header capture is opt-in; without this, only the query leaks
Effect.provideService(HttpClient.TracerHeaderFilter, () => true)
)
)
Output:
url.full http://x/join?token=s3cret
url.query token=s3cret
http.request.header.x-token s3cret
The same token passed straight to HttpClientRequest.setUrlParam as a Redacted is recorded as <redacted>.
Cause
As in #9838, HttpApiClient encodes the query and the headers with their schemas, which turns a Redacted into its plain value, and puts the encoded values into the request (HttpApiClient.ts L552–L562). The client span then redacts only by name (HttpClient.ts L731–L739):
- Query parameters: the names in
UrlParams.CurrentRedactedNames, plus the names of parameters whose value is stillRedacted(tracing.tsL40–L52). After encoding, none are. - Headers: the names in
Headers.CurrentRedactedNames.
The endpoint's schema still knows which fields are Redacted. Bodies are not recorded in spans, so only the query and the headers are affected.
Possible fix
HttpApiClient could collect, per endpoint, the query and header fields whose schema contains a Redacted, and add them to UrlParams.CurrentRedactedNames and Headers.CurrentRedactedNames while it executes that endpoint's request. Alternatively, for the query, it could keep those values Redacted in the request's UrlParams, which the span already redacts.
Workaround
Add the names yourself around the client:
const withRedactedNames = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
Effect.flatMap(
Effect.all([Effect.service(UrlParams.CurrentRedactedNames), Effect.service(Headers.CurrentRedactedNames)]),
([urlParams, headers]) =>
effect.pipe(
Effect.provideService(UrlParams.CurrentRedactedNames, [...urlParams, "token"]),
Effect.provideService(Headers.CurrentRedactedNames, [...headers, "x-token"])
)
)
// HttpApiClient.make(Api, { transformClient: HttpClient.transform(withRedactedNames) })
The Docker Engine client I'm building does this for its registry headers and secret query parameters.
- Dominant language
- TypeScript
- Stars
- 16.7k
- Forks
- 808
- Avg merge
- 10h 33m
- Merged PRs (30d)
- 830
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Effect-TS/effect
-
RpcTest.makeClient: accept disableFatalDefectsPossibly taken @effect-bot claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Effect-TS/effect#9893 · 3 comments ·
Maintainers usually reply within 1 day
-
`HttpApiSchema.Upgrade`: allow a regular success for servers that don't upgradePossibly taken @effect-bot claimed this today. Opendocker-made-me-do-it enhancement
Effect-TS/effect#9912 · 1 assignee ·
Maintainers usually reply within 1 day
-
docker-made-me-do-it enhancement
Difficulty 5/5 Over a week Newbie friendliness 20/100
Maintainers usually reply within 1 day
-
`HttpApiSchema`: stream bodies in a custom framingPossibly taken @effect-bot claimed this today. Opendocker-made-me-do-it enhancement
Effect-TS/effect#9910 · 1 assignee ·
Maintainers usually reply within 1 day
-
`HttpApiSchema` streams: declare every media type a stream can be sent asPossibly taken @effect-bot claimed this today. Opendocker-made-me-do-it enhancement
Effect-TS/effect#9909 · 1 assignee ·
Maintainers usually reply within 1 day
All issues in Effect-TS/effect
Similar issues
-
[Bug]: Device screenshot fails with 500 on iPhone Duo (Xcode 27.2): serve-sim calls simctl without --displayPossibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
pingdotgg/t3code#24900 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
inu-appcenter/UNIDorm-web#411 ·
Maintainers usually reply within 1 day
-
🐛 bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
ranxianglei/billion-context#4720 ·
Maintainers usually reply within 1 day
-
area:arcade game p3-medium ready-for-agent type:bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
fderuiter/deruiter.dev#2840 ·
Maintainers usually reply within 1 day