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

1import typing 

2 

3from ._content import Content 

4from ._streams import ByteStream, Stream 

5from ._headers import Headers 

6 

7__all__ = ["Response"] 

8 

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} 

76 

77 

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"") 

89 

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)}') 

102 

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)) 

115 

116 @property 

117 def reason_phrase(self): 

118 return _codes.get(self.code, "Unknown Status Code") 

119 

120 def read(self): 

121 self.body = b"".join([part for part in self.stream]) 

122 self.stream = ByteStream(self.body) 

123 

124 def __repr__(self): 

125 return f"<Response [{self.code} {self.reason_phrase}]>"