You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 05a4319
Browse filesBrowse the repository at this point in the historyBrowse files
docs: fix stale facts and plain up user-facing prose (#151)
Bring README, docs site, demos, SECURITY and contributing guide up to date
with the circuit breaker and timeout middleware, fix misleading statements,
and remove dash-heavy, bold-labelled, all-caps phrasing.
pip install httpware[all] # pydantic, msgspec, and otel
47
48
```
48
49
49
50
## Quickstart
@@ -71,22 +72,24 @@ async def main() -> None:
71
72
asyncio.run(main())
72
73
```
73
74
74
-
The sync `Client`is identical — swap `AsyncClient` → `Client` and drop the `await`/`async with`. A 4xx/5xx response raises a typed `StatusError`; a malformed body raises `DecodeError`. Both subclass `httpware.ClientError`.
75
+
The sync `Client`works the same way: use `Client` instead of `AsyncClient`, and drop `await`and`async with`. A 4xx/5xx response raises a typed `StatusError`; a malformed body raises `DecodeError`. Both subclass `httpware.ClientError`.
75
76
76
77
## Documentation
77
78
78
-
Full guides live at **[httpware.modern-python.org](https://httpware.modern-python.org)**:
79
+
Full guides live at [httpware.modern-python.org](https://httpware.modern-python.org):
79
80
80
-
-**[Quickstart & observability](https://httpware.modern-python.org/)** — resilience middleware, streaming, and the stable logger/event contract.
81
-
-**[Middleware](https://httpware.modern-python.org/middleware/)** — write your own (auth, tracing, request-ID propagation).
Copy file name to clipboardExpand all lines: SECURITY.md
+9-9Lines changed: 9 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,18 +1,18 @@
1
-
# Security Policy
1
+
# Security policy
2
2
3
-
## Reporting a Vulnerability
3
+
## Reporting a vulnerability
4
4
5
5
If you discover a security vulnerability in `httpware`, please report it privately via [GitHub Security Advisories](https://github.com/modern-python/httpware/security/advisories/new).
6
6
7
-
**Do not file a public GitHub issue for security reports.**
7
+
Do not file a public GitHub issue for security reports.
8
8
9
-
## Disclosure Timeline
9
+
## Disclosure timeline
10
10
11
-
- We commit to acknowledging your report within **7 days**.
12
-
- We aim to provide a fix or detailed mitigation plan within **30 days** of confirmation.
13
-
- We follow a **90-day private disclosure window** before public disclosure of the vulnerability and fix, unless a coordinated earlier disclosure is in the interest of users (e.g., the vulnerability is already being actively exploited).
11
+
- We acknowledge reports within 7 days.
12
+
- We aim to provide a fix or a detailed mitigation plan within 30 days of confirming the report.
13
+
- We keep a vulnerability and its fix private for 90 days before disclosing them, unless an earlier coordinated disclosure serves users better, for example when the vulnerability is already being exploited.
14
14
15
-
## Supported Versions
15
+
## Supported versions
16
16
17
17
Security fixes are provided for:
18
18
@@ -31,5 +31,5 @@ In scope:
31
31
32
32
Out of scope:
33
33
34
-
- Vulnerabilities in transitive dependencies (`httpx2`, `pydantic`, etc.) — report those upstream. We will fast-track a `httpware` release pinning the patched version once an upstream fix is available.
34
+
- Vulnerabilities in dependencies such as `httpx2` or `pydantic`. Report those upstream; once a fix is released there, we will quickly publish an httpware release that requires the patched version.
Copy file name to clipboardExpand all lines: docs/decoders.md
+21-22Lines changed: 21 additions & 22 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,8 +1,8 @@
1
1
# Decoders
2
2
3
-
`httpware`'s typed-response extension point is the **`ResponseDecoder` protocol**. A decoder turns raw response bytes into a typed object: when you pass `response_model=` to `send` / `send_with_response`, the client walks its decoder list, picks the first one that claims your model, and hands it the body.
3
+
A decoder turns raw response bytes into a typed object. When you pass `response_model=` to a request, the client goes through its decoder list, picks the first decoder that accepts your type, and hands it the body.
4
4
5
-
The built-in `PydanticDecoder` and `MsgspecDecoder`are themselves implementations of this protocol; nothing about them is privileged. Reach for a custom decoder when you need a body **format** the built-ins don't speak (CSV, XML, MessagePack, a bespoke binary frame) or a **type system**they don't cover (`attrs`, `marshmallow`, your own class hierarchy). If pydantic or msgspec already decodes your model, you don't need one — see [When NOT to write a decoder](#when-not-to-write-a-decoder).
5
+
`PydanticDecoder` and `MsgspecDecoder`implement the same `ResponseDecoder`protocol you would. Write your own when the body is in a format the built-ins can't read (CSV, XML, MessagePack, a custom binary format) or the type comes from a library they don't support (`attrs`, `marshmallow`, your own classes). If pydantic or msgspec already decodes your type, you don't need one; see [When not to write a decoder](#when-not-to-write-a-decoder).
6
6
7
7
## The protocol
8
8
@@ -20,30 +20,29 @@ class ResponseDecoder(Protocol):
`can_decode(model)` decides whether the decoder takes a type. The client asks each decoder in `decoders=[...]` order and uses the first one that returns `True`. Accept every type you can handle and let the list order express the caller's preference, but reject types that belong to another library: a CSV decoder should not accept a `pydantic.BaseModel`. `can_decode` must never raise. It runs before the request and outside the `DecodeError` wrapping that protects `decode`, so an exception there reaches the caller as something other than a `ClientError`. If the decoder can't tell, return `False`.
24
24
25
-
-**`can_decode(model) -> bool`** — the dispatch predicate. The client walks `decoders=[...]` in order and picks the **first** decoder that returns `True`. Claim every model you can actually handle (broad is correct — list ordering, not narrow predicates, encodes the caller's preference), but **reject another library's native types**: a CSV decoder has no business claiming a `pydantic.BaseModel`. `can_decode`**MUST NOT raise** — it runs at dispatch time, before the HTTP call and *outside* the `DecodeError` wrap that protects `decode`, so an exception here escapes `httpware`'s `ClientError` contract instead of being translated. A decoder that can't decide must return `False` (decline), not raise.
26
-
-**`decode(content, model) -> T`** — the decode itself, raw response bytes in, a `model` instance out. Any exception you raise here is caught by the client and wrapped as `httpware.DecodeError` (carrying `response`, `model`, and the `original` exception). You do **not** need to raise `DecodeError` yourself — raise whatever your parser raises and let the seam translate it.
25
+
`decode(content, model)` takes the raw body and returns an instance of `model`. The client wraps any exception it raises in `httpware.DecodeError`, with `response`, `model` and the `original` exception attached, so raise whatever your parser raises.
27
26
28
-
The protocol is `@runtime_checkable` and structural: any object with these two methods satisfies it. You do not subclass anything.
27
+
The protocol is structural and `@runtime_checkable`: any object with these two methods satisfies it, with no base class.
29
28
30
29
## How the client resolves a model
31
30
32
-
Both clients take `decoders: Sequence[ResponseDecoder] | None = None`, composed once at `__init__` and frozen for the client's lifetime.
31
+
Both clients take `decoders: Sequence[ResponseDecoder] | None = None`. The list is fixed when the client is built.
33
32
34
-
-**Order is preference.**`decoders=[CsvDecoder(), PydanticDecoder()]` asks the CSV decoder first; pydantic only sees models CSV declined. List position is how you disambiguate a shape two decoders could both claim.
35
-
-**`decoders=None`** resolves against installed extras — pydantic-first when both are present, either-only when one is, an empty tuple when neither. To *add* a decoder without losing the built-ins, list them explicitly: `decoders=[CsvDecoder(), PydanticDecoder()]`.
36
-
-**No claimer is a pre-flight error.** When `response_model=` is set and no decoder claims it, the client raises `MissingDecoderError`**before** sending the request — you find out at wiring time, not after a wasted round-trip. This is distinct from `DecodeError`: `MissingDecoderError` means *nothing handles this model* (fix: install an extra or pass `decoders=[...]`); `DecodeError` means *a decoder ran and the payload was malformed* (fix: the server or the model). See [Errors](errors.md).
33
+
- Order is preference. With `decoders=[CsvDecoder(), PydanticDecoder()]`, pydantic only sees the types the CSV decoder declined. When two decoders could both handle a type, the earlier one wins.
34
+
-`decoders=None` uses the installed extras: pydantic then msgspec when both are installed, whichever one is installed, or no decoders at all. Passing a list replaces the defaults, so include the built-ins you still want: `decoders=[CsvDecoder(), PydanticDecoder()]`.
35
+
-If `response_model=` is set and no decoder accepts it, the client raises `MissingDecoderError` before sending the request. That means nothing handles the type, and the fix is to install an extra or pass `decoders=[...]`. A `DecodeError`instead means a decoder ran and the body didn't fit the type, which points at the server or the model. See [Errors](errors.md).
37
36
38
-
## Decoders are sync — for both clients
37
+
## One sync protocol for both clients
39
38
40
-
Unlike middleware, which has separate `AsyncMiddleware`and `Middleware` flavors, there is **one**`ResponseDecoder` protocol, shared by `AsyncClient` and `Client` alike. `decode` is a synchronous method: by the time it runs, the body has already been read off the wire, so decoding is pure CPU work with nothing to await. Write one decoder and pass it to either client.
39
+
Middleware comes in sync and async versions, but there is only one `ResponseDecoder` protocol, used by both clients. `decode` is synchronous because the body has already been read when it runs, so there is nothing to await. The same decoder works with either client.
41
40
42
41
## Writing your own
43
42
44
43
### Worked example: a CSV decoder
45
44
46
-
A decoder for`text/csv`endpoints that returns a `list` of dataclass rows. Both built-ins are JSON, so this is the case they can't cover — and it shows the seam's real shape: raw bytes in, typed object out, no JSON anywhere.
45
+
This decoder reads`text/csv`responses into a `list` of dataclass rows. Both built-ins only read JSON, so they can't handle this case.
47
46
48
47
```python
49
48
import csv
@@ -77,7 +76,7 @@ class CsvDecoder:
77
76
return [row_type(**{name: field_types[name](value) for name, value in row.items()}) for row in reader]
78
77
```
79
78
80
-
`can_decode`is total and never raises: a non-`list`model, a bare `list`, or`list[int]` all fall through to `False`. `decode`coerces each CSV cellwith its field's type (CSV values arrive as strings) — a real decoder would handle optionals, dates, and missing columns; this is where your domain logic goes. Wire it ahead of the built-ins so it gets first refusal on `list[...]`models while pydantic still handles everything else:
79
+
`can_decode` never raises: a non-`list`type, a bare `list` and`list[int]` all return `False`. `decode`converts each CSV cell, which arrives as a string, with its field's type. A real decoder would also handle optional fields, dates and missing columns. Put it before the built-ins so it sees `list[...]`types first, while pydantic still handles everything else:
81
80
82
81
```python
83
82
@dataclasses.dataclass
@@ -103,7 +102,7 @@ The same decoder instance works with a sync `Client(decoders=[CsvDecoder(), Pyda
103
102
104
103
### A note on claiming the right models
105
104
106
-
`can_decode`is a contract with the *rest of the list*. Claim too broadly and you steal models from decoders behind you; claim too narrowly and your decoder never runs. The rule of thumb: claim exactly the types you natively own, and reject another library's. An adapter for a third-party type system narrows its claim to that system — for example, a[`cattrs`](https://catt.rs)-backed decoder for `attrs` classes:
105
+
`can_decode`affects the other decoders in the list. Accept too much and you take types from the decoders after yours; accept too little and yours never runs. Accept exactly the types your decoder is for, and reject types from other libraries. A decoder for a third-party type system should accept only that system's types, as in this[`cattrs`](https://catt.rs) decoder for `attrs` classes:
Note this decoder is **two-pass** (`json.loads`, then `structure`). The built-in adapters deliberately decode in a single bytes-in pass (`TypeAdapter.validate_json`, `msgspec.json.Decoder.decode`) to skip the intermediate `dict` allocation — but that's a *performance choice for the built-ins*, not a protocol obligation. A custom decoder may go two-pass when its underlying library only structures from native Python objects; you pay one extra allocation, nothing more.
124
+
This decoder makes two passes: `json.loads`, then `structure`. The built-ins decode straight from bytes(`TypeAdapter.validate_json`, `msgspec.json.Decoder.decode`) to avoid building an intermediate `dict`, but that is only an optimization. Two passes are fine when your library can only work from Python objects; the cost is one extra allocation.
126
125
127
-
### When NOT to write a decoder
126
+
### When not to write a decoder
128
127
129
-
-**Your model is JSON.** Dataclasses, `TypedDict`s, primitives, pydantic models, and msgspec `Struct`s are all covered by the built-in `PydanticDecoder` / `MsgspecDecoder`. Install the extra (`httpware[pydantic]` or `httpware[msgspec]`) instead of writing a decoder.
130
-
-**You only want raw bytes or text.** Don't pass`response_model=`at all — call `send` (or a verb method) without it and read `response.content`/`response.text` directly. Decoders are for *typed* bodies.
131
-
-**The transform is per-call, not per-type.** If the shaping depends on the request rather than the model, it's a [middleware](middleware.md) concern, not a decoder.
128
+
-The body is JSON.`PydanticDecoder` and `MsgspecDecoder` handle dataclasses, `TypedDict`s, primitives, pydantic models and msgspec `Struct`s. Install `httpware[pydantic]` or `httpware[msgspec]`.
129
+
- You want raw bytes or text. Leave out`response_model=` and read `response.content`or`response.text`.
130
+
- The shaping depends on the request rather than the type. That belongs in [middleware](middleware.md).
132
131
133
132
## See also
134
133
135
-
-**`src/httpware/decoders/pydantic.py` and `msgspec.py`** — the built-in adapters as reference implementations, including how they memoize a `can_decode`verdict and cache the underlying parser per model.
136
-
-**[Quick-Start: typed responses](index.md)** — composing `response_model=` with the default decoder list.
134
+
-[`src/httpware/decoders/`](https://github.com/modern-python/httpware/tree/main/src/httpware/decoders): the built-in decoders, including how they cache `can_decode`results and parsers per type.
135
+
-[Quickstart: typed responses](index.md#typed-responses): `response_model=` with the default decoders.
0 commit comments