Coverage for src/httpx/_response.py: 97%
34 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-06-09 10:38 +0100
« prev ^ index » next coverage.py v7.6.12, created at 2025-06-09 10:38 +0100
1import typing
3from ._content import Content
4from ._streams import ByteStream, Stream
5from ._headers import Headers
7__all__ = ["Response"]
9# We're using the same set as stdlib `http.HTTPStatus` here...
10#
11# https://github.com/python/cpython/blob/main/Lib/http/__init__.py
12_codes = {
13 100: "Continue",
14 101: "Switching Protocols",
15 102: "Processing",
16 103: "Early Hints",
17 200: "OK",
18 201: "Created",
19 202: "Accepted",
20 203: "Non-Authoritative Information",
21 204: "No Content",
22 205: "Reset Content",
23 206: "Partial Content",
24 207: "Multi-Status",
25 208: "Already Reported",
26 226: "IM Used",
27 300: "Multiple Choices",
28 301: "Moved Permanently",
29 302: "Found",
30 303: "See Other",
31 304: "Not Modified",
32 305: "Use Proxy",
33 307: "Temporary Redirect",
34 308: "Permanent Redirect",
35 400: "Bad Request",
36 401: "Unauthorized",
37 402: "Payment Required",
38 403: "Forbidden",
39 404: "Not Found",
40 405: "Method Not Allowed",
41 406: "Not Acceptable",
42 407: "Proxy Authentication Required",
43 408: "Request Timeout",
44 409: "Conflict",
45 410: "Gone",
46 411: "Length Required",
47 412: "Precondition Failed",
48 413: "Content Too Large",
49 414: "URI Too Long",
50 415: "Unsupported Media Type",
51 416: "Range Not Satisfiable",
52 417: "Expectation Failed",
53 418: "I'm a Teapot",
54 421: "Misdirected Request",
55 422: "Unprocessable Content",
56 423: "Locked",
57 424: "Failed Dependency",
58 425: "Too Early",
59 426: "Upgrade Required",
60 428: "Precondition Required",
61 429: "Too Many Requests",
62 431: "Request Header Fields Too Large",
63 451: "Unavailable For Legal Reasons",
64 500: "Internal Server Error",
65 501: "Not Implemented",
66 502: "Bad Gateway",
67 503: "Service Unavailable",
68 504: "Gateway Timeout",
69 505: "HTTP Version Not Supported",
70 506: "Variant Also Negotiates",
71 507: "Insufficient Storage",
72 508: "Loop Detected",
73 510: "Not Extended",
74 511: "Network Authentication Required",
75}
78class Response:
79 def __init__(
80 self,
81 code: int,
82 *,
83 headers: Headers | typing.Mapping[str, str] | None = None,
84 content: Content | Stream | bytes | None = None,
85 ):
86 self.code = code
87 self.headers = Headers(headers)
88 self.stream : Stream = ByteStream(b"")
90 if content is not None:
91 if isinstance(content, bytes):
92 self.stream = ByteStream(content)
93 elif isinstance(content, Stream):
94 self.stream = content
95 elif isinstance(content, Content):
96 # Eg. Response(200, content=HTML(...))
97 stream, content_type = content.encode()
98 self.headers = self.headers.copy_set("Content-Type", content_type)
99 self.stream = stream
100 else:
101 raise TypeError(f'Expected `Content | Stream | bytes | None` got {type(content)}')
103 # https://datatracker.ietf.org/doc/html/rfc2616#section-4.3
104 # RFC 2616, Section 4.3, Message Body.
105 #
106 # All 1xx (informational), 204 (no content), and 304 (not modified) responses
107 # MUST NOT include a message-body. All other responses do include a
108 # message-body, although it MAY be of zero length.
109 if code >= 200 and code != 204 and code != 304:
110 content_length: int | None = self.stream.size
111 if content_length is None:
112 self.headers = self.headers.copy_set("Transfer-Encoding", "chunked")
113 else:
114 self.headers = self.headers.copy_set("Content-Length", str(content_length))
116 @property
117 def reason_phrase(self):
118 return _codes.get(self.code, "Unknown Status Code")
120 def read(self):
121 self.body = b"".join([part for part in self.stream])
122 self.stream = ByteStream(self.body)
124 def __repr__(self):
125 return f"<Response [{self.code} {self.reason_phrase}]>"