Aswin f8c4accd54 Reset
Reset dev
2025-03-18 00:17:27 +05:30

229 lines
7.8 KiB
Python

# -*- coding: utf-8 -*-
"""
hyper/http20/response
~~~~~~~~~~~~~~~~~~~~~
Contains the HTTP/2 equivalent of the HTTPResponse object defined in
httplib/http.client.
"""
import logging
import zlib
from ..common.decoder import DeflateDecoder
from ..common.headers import HTTPHeaderMap
log = logging.getLogger(__name__)
def strip_headers(headers):
"""
Strips the headers attached to the instance of any header beginning
with a colon that ``hyper`` doesn't understand. This method logs at
warning level about the deleted headers, for discoverability.
"""
# Convert to list to ensure that we don't mutate the headers while
# we iterate over them.
for name in list(headers.keys()):
if name.startswith(b':'):
del headers[name]
class HTTP20Response(object):
"""
An ``HTTP20Response`` wraps the HTTP/2 response from the server. It
provides access to the response headers and the entity body. The response
is an iterable object and can be used in a with statement (though due to
the persistent connections used in HTTP/2 this has no effect, and is done
soley for compatibility).
"""
def __init__(self, headers, stream):
#: The reason phrase returned by the server. This is not used in
#: HTTP/2, and so is always the empty string.
self.reason = ''
status = headers[b':status'][0]
strip_headers(headers)
#: The status code returned by the server.
self.status = int(status)
#: The response headers. These are determined upon creation, assigned
#: once, and never assigned again.
self.headers = headers
# The response trailers. These are always intially ``None``.
self._trailers = None
# The stream this response is being sent over.
self._stream = stream
# We always read in one-data-frame increments from the stream, so we
# may need to buffer some for incomplete reads.
self._data_buffer = b''
# This object is used for decompressing gzipped request bodies. Right
# now we only support gzip because that's all the RFC mandates of us.
# Later we'll add support for more encodings.
# This 16 + MAX_WBITS nonsense is to force gzip. See this
# Stack Overflow answer for more:
# http://stackoverflow.com/a/2695466/1401686
if b'gzip' in self.headers.get(b'content-encoding', []):
self._decompressobj = zlib.decompressobj(16 + zlib.MAX_WBITS)
elif b'deflate' in self.headers.get(b'content-encoding', []):
self._decompressobj = DeflateDecoder()
else:
self._decompressobj = None
@property
def trailers(self):
"""
Trailers on the HTTP message, if any.
.. warning:: Note that this property requires that the stream is
totally exhausted. This means that, if you have not
completely read from the stream, all stream data will be
read into memory.
"""
if self._trailers is None:
self._trailers = self._stream.gettrailers() or HTTPHeaderMap()
strip_headers(self._trailers)
return self._trailers
def read(self, amt=None, decode_content=True):
"""
Reads the response body, or up to the next ``amt`` bytes.
:param amt: (optional) The amount of data to read. If not provided, all
the data will be read from the response.
:param decode_content: (optional) If ``True``, will transparently
decode the response data.
:returns: The read data. Note that if ``decode_content`` is set to
``True``, the actual amount of data returned may be different to
the amount requested.
"""
if amt is not None and amt <= len(self._data_buffer):
data = self._data_buffer[:amt]
self._data_buffer = self._data_buffer[amt:]
response_complete = False
elif amt is not None:
read_amt = amt - len(self._data_buffer)
self._data_buffer += self._stream._read(read_amt)
data = self._data_buffer[:amt]
self._data_buffer = self._data_buffer[amt:]
response_complete = len(data) < amt
else:
data = b''.join([self._data_buffer, self._stream._read()])
response_complete = True
# We may need to decode the body.
if decode_content and self._decompressobj and data:
data = self._decompressobj.decompress(data)
# If we're at the end of the request, we have some cleaning up to do.
# Close the stream, and if necessary flush the buffer.
if response_complete:
if decode_content and self._decompressobj:
data += self._decompressobj.flush()
if self._stream.response_headers:
self.headers.merge(self._stream.response_headers)
# We're at the end, close the connection.
if response_complete:
self.close()
return data
def read_chunked(self, decode_content=True):
"""
Reads chunked transfer encoded bodies. This method returns a generator:
each iteration of which yields one data frame *unless* the frames
contain compressed data and ``decode_content`` is ``True``, in which
case it yields whatever the decompressor provides for each chunk.
.. warning:: This may yield the empty string, without that being the
end of the body!
"""
while True:
data = self._stream._read_one_frame()
if data is None:
break
if decode_content and self._decompressobj:
data = self._decompressobj.decompress(data)
yield data
if decode_content and self._decompressobj:
yield self._decompressobj.flush()
self.close()
return
def fileno(self):
"""
Return the ``fileno`` of the underlying socket. This function is
currently not implemented.
"""
raise NotImplementedError("Not currently implemented.")
def close(self):
"""
Close the response. In effect this closes the backing HTTP/2 stream.
:returns: Nothing.
"""
self._stream.close()
# The following methods implement the context manager protocol.
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
return False # Never swallow exceptions.
class HTTP20Push(object):
"""
Represents a request-response pair sent by the server through the server
push mechanism.
"""
def __init__(self, request_headers, stream):
#: The scheme of the simulated request
self.scheme = request_headers[b':scheme'][0]
#: The method of the simulated request (must be safe and cacheable,
#: e.g. GET)
self.method = request_headers[b':method'][0]
#: The authority of the simulated request (usually host:port)
self.authority = request_headers[b':authority'][0]
#: The path of the simulated request
self.path = request_headers[b':path'][0]
strip_headers(request_headers)
#: The headers the server attached to the simulated request.
self.request_headers = request_headers
self._stream = stream
def get_response(self):
"""
Get the pushed response provided by the server.
:returns: A :class:`HTTP20Response <hyper.HTTP20Response>` object
representing the pushed response.
"""
return HTTP20Response(self._stream.getheaders(), self._stream)
def cancel(self):
"""
Cancel the pushed response and close the stream.
:returns: Nothing.
"""
self._stream.close(8) # CANCEL