The error message contained no fake API key. Exporting the same exception still included it.
I found that while testing the public SDKs for odds-api.net, the data service I run. I deliberately put a fake key into a mocked error response, then followed it through the exception and a sample support export. The exercise used no real credentials or customer requests.
That is an easy result to miss if the check stops at the message printed in the console. The request log might also show a redacted authentication header. Both can look fine while another copy of the key is still sitting inside the object someone sends to support.
1. Follow the copies of the request
Take a failed API request. A developer has its headers, a curl command for reproducing it, and an exception returned by the client library. A support bundle could collect all three.
Those copies need separate checks. Replacing the key in the header dictionary does nothing to the curl command that was built from it earlier. Removing a field named api_key does nothing to a credential inside a string named message.
For the test, I planted the fake key inside a header and a copied curl command, nested under an array of diagnostic attempts in the synthetic response. This gave the exporter two copies to deal with, including one stored as ordinary text.
Our Python SDK wrapped that response in an exception. Its display message contained the HTTP status, while its body attribute retained the diagnostic information. Printing the message excluded the key. Serializing the exception’s attributes included it.
The TypeScript SDK behaved the same way in the synthetic checks. Its quickstart also logs the status and error body, which gives an application another place to copy whatever that body contains.
Keeping an error body is useful when you are working out what failed. The decision about what leaves the application still needs to happen when that information is logged, copied or exported.
2. Decide what support needs to keep
It is tempting to send the whole object because you do not yet know which part will explain the failure. But that gives the exporter every nested field, copied command and raw response to inspect.
For this failed request, I started with four fields: the request ID, HTTP method, route and status. That was enough to identify the request and classify the HTTP failure without carrying its authentication details.
def support_record(request_id, status):
bundle = support_record(“req-demo-001”, 502)
assert “FAKE_SUPPORT_KEY_NOT_REAL” not in exported
assert bundle[“request_id”] == “req-demo-001”
assert bundle[“status”] == 502
In the full reproduction, the request ID comes from the test’s request metadata and the status comes from the caught exception. The original exception still contains the fake key. The smaller record keeps the context we selected and leaves out the diagnostic body entirely.
That is a field allowlist: decide what belongs in the record, then build it from those fields. If someone later adds a raw response or another debug object to the exception, it does not automatically become part of the support export.
There is a limit to what the field names can tell you, though. A request ID or error description can contain unexpected text. Validate allowed values for their expected type, format and length. Take the method and route from application request metadata, and prefer a route template over a full URL containing query parameters.
If support needs more context, add a specific field for that purpose. A stable error code or retry count is easier to review than an unrestricted debug object.
The check that matters runs after the exporter has finished assembling and serializing the record. A clean header dictionary proves little about an error body added later.
In the reproduction, I checked that the fake key was present in the whole-exception export and absent from the smaller record. I also checked that the request ID and status survived. An empty export would pass a secret-absence test and give support nothing useful.
Put the fake key in nested dictionaries, arrays, copied commands and plain-text errors. Include a case where the header has been cleaned but the copied command still contains it. If the exporter transforms values, test the encoded or escaped versions too.
Both SDKs also retained non-JSON error text in the checks I ran. A response that fails JSON parsing still needs an export policy.
For an archive, unpack the finished bundle and inspect every included file. The useful test is the artifact someone will attach to the ticket, including any logs or diagnostic files collected alongside the main record.
Take one failed request through the steps your team uses when asking for help. If the main log is clean, follow the next copy. Keep going until you have checked what actually reaches support.
Rhyan Morice runs odds-api.net, a sports data API. He works on API integrations, streaming data and developer tooling.
