fix(textract): normalise env-derived config so quoted vars cannot poison the SDK - #274
Open
mashhoodr wants to merge 1 commit into
Open
fix(textract): normalise env-derived config so quoted vars cannot poison the SDK#274mashhoodr wants to merge 1 commit into
mashhoodr wants to merge 1 commit into
Conversation
…son the SDK NIETE ran with AWS_REGION_TEXTRACT set to the 16-character value `"ap-southeast-1"` — the region WITH literal double quotes — and both AWS_TEXTRACT_ACCESS_KEY_ID (22 chars for a 20-char key) and AWS_TEXTRACT_SECRET_ACCESS_KEY (42 for 40) quoted the same way, on both the bot and sqs-worker services. @smithy rejected the region (`Region not accepted: region=""ap-southeast-1""`), so every Textract call threw. Because Textract is the lesson-plan OCR fallback and that throw propagates, it terminated the coaching session: 30 sessions hit it and 23 teachers never received their report. A .env file would have been fine — dotenv strips wrapping quotes when it parses a file. A platform env var is delivered verbatim, so quotes pasted into a dashboard become part of the value. That asymmetry is the bug, and it is not specific to this call site. envStr() trims and removes ONE layer of balanced quotes, returning undefined for empty so the existing `A || B` fallback chains behave identically. It warns once per variable rather than repairing silently, because the operator needs to learn which var is malformed; and it does not throw, because config normalisation must not become a new way to fail at boot. Unbalanced quotes are left alone — intent is unknowable, and inventing a value is worse than failing loudly. Applied to the region AND the credentials, including assertTextractCredentialsPresent(): using raw process.env there would let a value of `""` pass as "present" while the SDK receives undefined, and a check that disagrees with what it guards produces the worst error reports. Note the override pair is what let this hide: AWS_TEXTRACT_* shadows a perfectly good generic AWS_* config, so nothing inspecting the generic vars could see it. The generic AWS_REGION was clean the whole time, which is why SQS never failed and only Textract did. Verified: 20 new tests pass; tests/config + tests/setup show the same 14 pre-existing suite failures before and after, so no regressions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What broke
NIETE ran with
AWS_REGION_TEXTRACT="ap-southeast-1"— the region with literal double-quote characters, 16 bytes for a 14-byte region.AWS_TEXTRACT_ACCESS_KEY_ID(22 chars for a 20-char key) andAWS_TEXTRACT_SECRET_ACCESS_KEY(42 for 40) were quoted the same way, on both thebotandsqs-workerservices — the signature of one block paste, not a typo.@smithy's validator formatsregion="${region}", which is why the error read with doubled quotes:Textract is the lesson-plan OCR fallback, and that throw propagates, so it terminated the coaching session. Verified identical in 30/30 occurrences:
lesson_plan_extraction_status='failed'on all 30, and 23 teachers never received a report.Because the credentials were quoted too, this path had never worked since the day it was configured. It only runs for lesson plans whose text layer is too thin for
pdf-parse, so it stayed latent until traffic grew enough to hit that case regularly.Why it's not a one-line fix at the call site
A
.envfile would have been fine — dotenv strips wrapping quotes when parsing. A platform env var (Railway, Fly, a k8s secret) is delivered verbatim, so quotes pasted into a dashboard become part of the value. That asymmetry is the real bug and it applies to any env-derived config, not just this one.Note the
AWS_TEXTRACT_*override pair is what let it hide: it shadows a perfectly good genericAWS_*config. The genericAWS_REGIONwas clean (us-east-1) the whole time — which is exactly why SQS never failed and only Textract did.The change
bot/shared/utils/env.js—envStr(name):undefinedfor unset/empty, so existingA || Bchains behave identicallyREGION_FRAMEWORK_MAPis pinned by a test)Applied to the region and the credentials, including
assertTextractCredentialsPresent()— reading rawprocess.envthere would let a value of""pass as "present" while the SDK receivesundefined, and a check that disagrees with what it guards produces the worst class of error report.Verification
tests/config/env-value.test.js, all passingtests/config+tests/setup(655 tests): the same 14 suite failures before and after — all pre-existing (dangling markdown link, schema conformance, etc.), zero regressionsNot in this PR
Two things surfaced during the investigation, deliberately left out:
UnsupportedDocumentException: Request has unsupported document format— synchronousAnalyzeDocumentwithDocument: { Bytes }does not accept PDF. First seen 07:17 UTC 2026-08-18, never before, because nothing ever got this far. 26 of the 30 affected sessions were PDFs, so this still needs its own fix.🤖 Generated with Claude Code