Repository navigation
feat: add Tesla.SecretString to keep secrets out of inspect output - #939
Conversation
PR SummaryLow Risk Overview
The client guide gains a “Secrets in middleware options” section describing why tokens in client/env inspect output are risky and how to wrap them. Tests cover inspect redaction, interpolation, headers unwrapping, and clients/envs that hold wrapped secrets without printing raw values in Reviewed by Cursor Bugbot for commit f43a030. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
🟡 Changes recommended
The new test helper that temporarily sets :tesla, :inspect restores prior config using a truthiness check that can mis-restore falsy values, risking incorrect global config cleanup.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR hardens Tesla’s developer-facing logging/inspection behavior by implementing a custom Inspect protocol for Tesla.Client that redacts middleware and adapter options by default, reducing the risk of credential leakage when %Tesla.Env{} (which embeds __client__) is inspected.
Changes:
- Add
Inspectimplementation forTesla.Clientthat prints module names but redacts options unlessconfig :tesla, inspect: :fullis set. - Add a new
@moduledoctoTesla.Clientdocumenting the redaction behavior and opt-in full inspection. - Add tests and guide documentation covering default redaction,
:fullmode, env embedding, andpostmiddleware rendering.
File summaries
| File | Description |
|---|---|
| lib/tesla/client.ex | Adds @moduledoc and a redacting Inspect implementation for Tesla.Client. |
| test/tesla/client_test.exs | Adds coverage for redacted vs full inspection, env embedding, and post middleware rendering. |
| guides/explanations/0.client.md | Documents the new default redaction behavior and how to opt into full inspect output. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟢 Approval recommended
The change is well-scoped, addresses a concrete secret-leak risk, and includes targeted tests and documentation to validate and explain the new behavior.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
lib/tesla/client.ex:224
- The redaction logic currently inspects Tesla’s internal runtime tuples (e.g.
{Module, :call, [opts]}) and re-implementsunruntime/1inside theInspectimpl. This couplesInspectto internal representation details and duplicates the unruntime logic already maintained inTesla.Client.adapter/1andTesla.Client.middleware/1, increasing the risk that future changes to runtime stack representation will update one place but not the other.
You can avoid this by first converting adapter/middleware stacks back to the public “user form” via Tesla.Client.adapter/1 / Tesla.Client.middleware/1 (using a temporary %Tesla.Client{pre: stack} for post), and then applying redaction to those user-form entries.
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Lite
fa5e5f6 to
0071e29
Compare
|
@BlueCollarChris, first of all, I strongly agree with the intent here. However, after thinking about it for a while, I wouldn’t take this approach for a few reasons:
Instead, I would make secrecy explicit by introducing a value type such as: defmodule Tesla.SecretString do
@opaque t :: %__MODULE__{value: String.t()}
defstruct [:value]
def new(value) when is_binary(value) do
%__MODULE__{value: value}
end
end
defimpl Inspect, for: Tesla.SecretString do
def inspect(_val, _opts) do
"Tesla.SecretString<redacted>"
end
end
defimpl String.Chars, for: Tesla.SecretString do
def to_string(secret) do
secret.value
end
end
Tesla.client([
{Tesla.Middleware.Headers,
[
{"authorization", Tesla.SecretString.new("Bearer #{token}")}
]}
])Middleware could then wrap sensitive values in Something around those lines, |
|
Ill take a look at this approach a bit more. |
|
@BlueCollarChris any updates? |
Was able to find some time and working on it today |
Middleware options live in the client, and every %Tesla.Env{} embeds its
client, so a token given to a middleware printed wherever a client or env
was inspected. Rather than redacting the client's Inspect output, make
secrecy explicit on the value: Tesla.SecretString is redacted by Inspect
and returns its value through String.Chars.
Tesla.Middleware.Headers unwraps secret values when it puts them on the
request; BearerAuth and BasicAuth accept them through interpolation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0071e29 to
f43a030
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
It does not implement the promised default Tesla.Client redaction, and the documentation overstates protection for populated environments.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1
Open (1)
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
BearerAuth still emits "******" instead of interpolating the token, so the new bearer-token test cannot pass.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 1


Problem
A
%Tesla.Client{}carries every middleware together with its options, and a%Tesla.Env{}embeds the client as__client__. Both use the defaultInspect, so this:writes
Bearer s3cretto the log. TheLogger.error("...#{inspect(env)}")pattern is common in error handling, so this tends to surface as a credential in a production log aggregator. We found it that way in a service wrapping a third-party API client built on Tesla: the provider API key appeared in the logs on every non-2xx response.Change
Following the review discussion, secrecy is made explicit on the value rather than by redacting the client's
Inspectoutput. This PR addsTesla.SecretString:Inspectalways renders#Tesla.SecretString<redacted>, however deeply the value is nested: in middleware options, in a%Tesla.Client{}, or in the__client__of a%Tesla.Env{}.String.Charsreturns the value, so middleware that builds a header by interpolation works with a wrapped value unchanged. That also means"#{secret}"prints it; the docs call this out.Tesla.Middleware.HeadersunwrapsTesla.SecretStringvalues when it puts them on the request.Tesla.Middleware.BearerAuth(:token) andTesla.Middleware.BasicAuth(:password) accept wrapped values with no code change, via interpolation; their option docs now mention it.Tesla.Client'sInspectoutput is unchanged. Nothing is redacted unless the caller wraps it.Usage:
Docs:
@moduledocforTesla.SecretString, a "Secret header values" section inTesla.Middleware.Headers, and a "Secrets in middleware options" section inguides/explanations/0.client.md.Not covered here
inspect(client)/inspect(env)until they wrap their values. Wrapping automatically (e.g. a build-time hook letting middleware wrap their own secret options) would be a separate proposal.Headers/BearerAuth/BasicAuthrun,env.headersholds the real value, since that is what goes on the wire. Inspecting the env mid-request (in a custom middleware, orTesla.Middleware.Loggerat debug level) still shows it;Tesla.Middleware.Logger's:filter_headerscovers the latter. Adapters replaceenv.headerswith the response headers, so a returned env does not carry them.Tesla.Middleware.Query) are not unwrapped here.Tests
test/tesla/secret_string_test.exs: redaction at any nesting,to_string/interpolation, and a client holding wrapped secrets inHeaders,BearerAuthandBasicAuththat prints none of them (directly or embedded in an env). Doctests run viadoctest Tesla.SecretString.Headers,BearerAuthandBasicAuthtests each gain a case putting a wrapped value on the request.