Skip to content

chore: input-validation pass — Request/Timeout/Limits/ClientConfig __post_init__ guards + charset parser fix#19

Merged
lesnik512 merged 5 commits into
mainfrom
chore/input-validation-pass
Jun 3, 2026
Merged

chore: input-validation pass — Request/Timeout/Limits/ClientConfig __post_init__ guards + charset parser fix#19
lesnik512 merged 5 commits into
mainfrom
chore/input-validation-pass

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Summary

Closes five entries in planning/deferred-work.md (Story 1-2 section). Spec: planning/specs/2026-06-03-input-validation-pass-design.md. Plan: planning/plans/2026-06-03-input-validation-pass-plan.md.

  • Charset parser inner-whitespace fix — adds one trailing .strip() to _parse_charset so Content-Type: ...; charset=" iso-8859-1 " returns "iso-8859-1" instead of " iso-8859-1 ". The other "concerns" in the deferred entry don't actually fire on the current code.
  • Request.__post_init__ — validates url is a non-empty str, all four mapping fields (headers, params, cookies, extensions) are Mappings, and every header/cookie name/value is a non-empty str with no CR/LF. Validation lives at the dataclass boundary so all with_* methods inherit it via dataclasses.replace with no per-method code.
  • Timeout.__post_init__ / Limits.__post_init__ — reject negative values for all 4 Timeout phase fields and all 3 Limits fields. Zero is permitted (Timeout zero = fail-immediately sentinel; Limits zero = downstream's call).
  • ClientConfig.__post_init__ — validates base_url is a non-empty str or None, strips trailing slash(es) so the stored value is canonical. Rejects "/"/"///" (would normalize to empty). Removes the now-redundant .rstrip("/") from AsyncClient._resolve_url since the stored value is already canonical.
  • planning/deferred-work.md — removes the five closed Story 1-2 entries.

Architecture: all validation lives in __post_init__ on the frozen dataclasses. Exception types: ValueError for invalid values, TypeError for wrong runtime types (one spec-sanctioned bundling: ClientConfig.base_url uses ValueError for both since the type and emptiness paths share the same actionable message).

This is a strict tightening. Code that previously silently accepted invalid inputs (empty URLs, CRLF in headers, negative timeouts, slash-only base_url, None mapping fields, etc.) now raises at construction. No public API surface change; with_* methods unchanged.

Five atomic commits.

Test plan

  • just lint-ci exits 0
  • just test — 335 passing (296 baseline + 39 new across the 5 implementation commits)
  • All *_rejects_* tests in tests/test_request.py and tests/test_config.py pass (validates each new rule fires)
  • Pre-existing tests in tests/test_client_methods.py, tests/test_middleware.py, tests/test_transports_httpx2.py all pass (no fixture regression from the new validation)
  • grep -E 'Charset parser|Header name/value|URL validation|with_query|Timeout.*Limits.*negative' planning/deferred-work.md returns empty
  • grep -n 'rstrip' src/httpware/client.py returns empty (redundant rstrip removed)

🤖 Generated with Claude Code

lesnik512 and others added 5 commits June 3, 2026 07:34
Adds one final .strip() after the quote-stripping chain in
_parse_charset so that Content-Type: ...; charset=" utf-8 " yields a
clean codec name. Python's codec registry happens to normalize
whitespace on lookup (so the end-to-end mojibake risk is currently
masked), but the parser should still return a clean value rather than
rely on codec leniency.

Test verifies _parse_charset directly (the unit-level contract) plus
an end-to-end smoke through Response.text.

The other "concerns" listed in the deferred-work entry (substring
false-positives, mismatched quotes, multi-charset directives) do not
actually fire on the current code, per the spec's analysis.

Closes deferred-work entry: "Charset parser robustness".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Request.__post_init__ that validates:
- url is a non-empty str
- headers, params, cookies, extensions are each a Mapping
- header and cookie names/values are non-empty str, no CR or LF

with_* methods inherit validation via dataclasses.replace; no
per-method code needed. Header validation is minimal per spec
(reject CR/LF, non-str, empty); full RFC 9110 token validation is
out of scope.

Closes deferred-work entries: "Header name/value validation",
"URL validation" (Request.url part), "with_query(None) handling".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both dataclasses now raise ValueError on construction if any field
is negative. Zero is permitted (Timeout zero = fail-immediately
sentinel; Limits zero = downstream's call on what it means, typically
"no limit").

Closes deferred-work entry: "Timeout / Limits negative-value
validation".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ClientConfig.__post_init__ now:
- rejects empty string / non-str base_url with ValueError
- strips trailing slash so the stored value is canonical

AsyncClient._resolve_url no longer does its own rstrip("/") on
base_url since the stored value is already canonical (DRY: one
source of truth for what a stored base_url looks like).

Closes deferred-work entry: "URL validation" (base_url normalization
part; the Request.url non-empty check shipped in the prior Request
__post_init__ commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes five Story 1-2 entries closed by this PR: charset parser
robustness, header name/value validation, URL validation, with_query
(None) handling, Timeout/Limits negative-value validation.

The remaining Story 1-2 entries (multi-valued query params, streaming
request bodies, @Final subclassing) stay open — they're different
shapes of change, explicitly out of scope for this pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lesnik512 lesnik512 self-assigned this Jun 3, 2026
@lesnik512 lesnik512 merged commit 42b3dc2 into main Jun 3, 2026
5 checks passed
@lesnik512 lesnik512 deleted the chore/input-validation-pass branch June 3, 2026 07:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant