Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

HttpApiClient: `Redacted` query and header values appear in plain text in client spans

Open
#9,882 3 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@effect-bot is already working on this.

Since Oct 24, 2026.

  • #9889 by @effect-bot — open

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 still Redacted (tracing.ts L40–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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Effect-TS/effect

All issues in Effect-TS/effect

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.