[Issue #11658] Relax CommonGrants URL pre-validation from strict to lax pydantic HttpUrl - #11659
Merged
Merged
Conversation
The dual-validator in url_utils.py pre-screened URLs with strict-mode
HttpUrl, inherited from when SDK <=0.5.x ran model_validate() with
model_config strict=True and would crash on RFC-invalid characters.
The SDK dropped model-level strict in 0.6.x, so the strict pre-check
now rejects URLs (e.g. raw {} in query strings, common in agency
NSPIRES links) that every validator in the actual response pipeline
accepts — dropping the source field and logging a WARNING for URLs
that would serve fine.
Lax mode matches the SDK model exactly; the marshmallow leg of the
dual-check still drops the genuinely batch-breaking shapes from #9904
(comma-separated URLs, TLD-less hosts). A round-trip test guards
against a future SDK version re-tightening URL validation.
Cuts history exposition from the validator docstring and the round-trip test, removes a transform-only NASA test that the round-trip test fully subsumes, and extracts the shared opp-data and response-load scaffolding used by the accept and reject halves of the dual-validator contract.
SnowboardTechie
marked this pull request as ready for review
July 27, 2026 19:38
karinamzalez
left a comment
Collaborator
There was a problem hiding this comment.
A few quick questions!
- Consumers will now get characters like {}, |, and ^ in query strings. The SDKs accept them, but is there anything downstream-- e.g. API docs, the contract, consumer code-- that expects clean URLs and might trip on these? (same Q for attachments since it looks like it uses the same URL helper)
- The 938 values that don't make it through will keep logging cg_event=url_validation_error. Since those are mostly bad data, is that log volume a concern, or are we treating them as a data-quality signal going forward?
Collaborator
Author
|
Good questions. Nothing downstream should trip on them. The marshmallow URL validator is what gates these fields on the way out, and it accepts On the logging: not a volume concern, about 3% of records hit it. Treating it as upstream data-quality signal going forward. The remaining values aren't links at all ( |
karinamzalez
self-requested a review
August 5, 2026 19:29
karinamzalez
approved these changes
Aug 5, 2026
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.
Summary
Fixes HHS/simpler-grants-protocol#1068
Prod logs a steady stream of
Dropping URL field from CG response: failed dual validationfor URLs the response pipeline serves fine, e.g. NASA NSPIRES links with raw{}in the query string. The strict-mode pre-check that drops them mirrors SDK behavior that no longer exists: the SDK dropped model-levelstrict=Truein 0.6.x (HHS/simpler-grants-protocol#555), leaving the pre-check stricter than every validator in the actual egress path._UrlValidatornow uses laxHttpUrl, matchingOpportunityBase.source.The #9904 search-500 protection is untouched. The dual-check's marshmallow leg still drops comma-separated URLs and TLD-less hosts, and
HttpUrlstill restricts schemes to http/https regardless of strictness (new test covers that).The old NASA-rejection test becomes a round-trip guard: URLs surviving
validate_url()must constructOpportunityBaseand load through the marshmallow response schema, so a future SDK that re-tightens URL validation fails CI instead of dropping fields in prod.Measured impact on real data
additional_info_urlcomes from legacyfd_link_url(transform_util.py:252), so I ran everyAdditionalInformationURLin the public grants.gov daily extract (GrantsDBExtract20260727v2, 32,170 distinct values across 82,975 records) through the dual-check both ways:The 778 are mostly trailing whitespace (497 distinct) and NSPIRES braces (245). Nothing in the corpus contains a tab, newline, or carriage return, so no URL gets silently rewritten by control-character stripping. The 938 still dropped are not links at all:
www.grants.gov,askcsid@fema.dhs.gov,See Invitation Letter,N/A. Remainingcg_event=url_validation_errorwarnings become upstream data-quality signal.Behavior notes
{},|,^) in query strings. Both SDK response validators accept them: the Python SDK's laxHttpUrland the TS SDK'sz.string().url().AttachmentValue.downloadUrlshares the helper, so attachments relax the same way, restoring the pre-[Issue #9904] Dual-validate URLs in CG transformation to fix search 500 #9911 behavior that [Issue #9904] Dual-validate URLs in CG transformation to fix search 500 #9911 flagged as an unintended tightening.&from HTML-escaped legacy data, sosourcecan be populated with a link that mis-targets. Roughly 1,480 such URLs already pass strict and are served today; this adds 244 to that count.Validation steps
All 110 pass locally with Postgres + OpenSearch up.
make format-check,make lint,make lint-security, andmake db-checkare clean.After deploy, confirm the NSPIRES record's
sourceis populated and nocg_event=url_validation_errorWARNING is logged for it: