Skip to content

Commit 6c57627

Browse files
Support informational responses with HTTP/1.1 (#581)
* Support information responses with HTTP/1.1 * Add tests for 101 protocol switch * Document handling a WebSocket session using the network_stream extension
1 parent f2af4c3 commit 6c57627

File tree

5 files changed

+103
-3
lines changed

5 files changed

+103
-3
lines changed

docs/extensions.md

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,9 @@ The interface provided by the network stream:
179179

180180
This API can be used as the foundation for working with HTTP proxies, WebSocket upgrades, and other advanced use-cases.
181181

182-
An example to demonstrate:
182+
##### `CONNECT` requests
183+
184+
A proxy CONNECT request using the network stream:
183185

184186
```python
185187
# Formulate a CONNECT request...
@@ -206,6 +208,52 @@ with httpcore.stream("CONNECT", url) as response:
206208
print(data)
207209
```
208210

211+
##### `Upgrade` requests
212+
213+
Using the `wsproto` package to handle a websockets session:
214+
215+
```python
216+
import httpcore
217+
import wsproto
218+
import os
219+
import base64
220+
221+
222+
url = "http://127.0.0.1:8000/"
223+
headers = {
224+
b"Connection": b"Upgrade",
225+
b"Upgrade": b"WebSocket",
226+
b"Sec-WebSocket-Key": base64.b64encode(os.urandom(16)),
227+
b"Sec-WebSocket-Version": b"13"
228+
}
229+
with httpcore.stream("GET", url, headers=headers) as response:
230+
if response.status != 101:
231+
raise Exception("Failed to upgrade to websockets", response)
232+
233+
# Get the raw network stream.
234+
network_steam = response.extensions["network_stream"]
235+
236+
# Write a WebSocket text frame to the stream.
237+
ws_connection = wsproto.Connection(wsproto.ConnectionType.CLIENT)
238+
message = wsproto.events.TextMessage("hello, world!")
239+
outgoing_data = ws_connection.send(message)
240+
network_steam.write(outgoing_data)
241+
242+
# Wait for a response.
243+
incoming_data = network_steam.read(max_bytes=4096)
244+
ws_connection.receive_data(incoming_data)
245+
for event in ws_connection.events():
246+
if isinstance(event, wsproto.events.TextMessage):
247+
print("Got data:", event.data)
248+
249+
# Write a WebSocket close to the stream.
250+
message = wsproto.events.CloseConnection(code=1000)
251+
outgoing_data = ws_connection.send(message)
252+
network_steam.write(outgoing_data)
253+
```
254+
255+
##### Extra network information
256+
209257
The network stream abstraction also allows access to various low-level information that may be exposed by the underlying socket:
210258

211259
```python

httpcore/_async/http11.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ async def _receive_response_headers(
153153

154154
while True:
155155
event = await self._receive_event(timeout=timeout)
156-
if isinstance(event, h11.Response):
156+
if isinstance(event, (h11.Response, h11.InformationalResponse)):
157157
break
158158

159159
http_version = b"HTTP/" + event.http_version

httpcore/_sync/http11.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def _receive_response_headers(
153153

154154
while True:
155155
event = self._receive_event(timeout=timeout)
156-
if isinstance(event, h11.Response):
156+
if isinstance(event, (h11.Response, h11.InformationalResponse)):
157157
break
158158

159159
http_version = b"HTTP/" + event.http_version

tests/_async/test_http11.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,29 @@ async def test_http11_request_to_incorrect_origin():
206206
async with AsyncHTTP11Connection(origin=origin, stream=stream) as conn:
207207
with pytest.raises(RuntimeError):
208208
await conn.request("GET", "https://other.com/")
209+
210+
211+
@pytest.mark.anyio
212+
async def test_http11_upgrade_connection():
213+
origin = Origin(b"https", b"example.com", 443)
214+
stream = AsyncMockStream(
215+
[
216+
b"HTTP/1.1 101 OK\r\n",
217+
b"Connection: upgrade\r\n",
218+
b"Upgrade: custom\r\n",
219+
b"\r\n",
220+
b"...",
221+
]
222+
)
223+
async with AsyncHTTP11Connection(
224+
origin=origin, stream=stream, keepalive_expiry=5.0
225+
) as conn:
226+
async with conn.stream(
227+
"GET",
228+
"https://example.com/",
229+
headers={"Connection": "upgrade", "Upgrade": "custom"},
230+
) as response:
231+
assert response.status == 101
232+
network_stream = response.extensions["network_stream"]
233+
content = await network_stream.read(max_bytes=1024)
234+
assert content == b"..."

tests/_sync/test_http11.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,29 @@ def test_http11_request_to_incorrect_origin():
206206
with HTTP11Connection(origin=origin, stream=stream) as conn:
207207
with pytest.raises(RuntimeError):
208208
conn.request("GET", "https://other.com/")
209+
210+
211+
212+
def test_http11_upgrade_connection():
213+
origin = Origin(b"https", b"example.com", 443)
214+
stream = MockStream(
215+
[
216+
b"HTTP/1.1 101 OK\r\n",
217+
b"Connection: upgrade\r\n",
218+
b"Upgrade: custom\r\n",
219+
b"\r\n",
220+
b"...",
221+
]
222+
)
223+
with HTTP11Connection(
224+
origin=origin, stream=stream, keepalive_expiry=5.0
225+
) as conn:
226+
with conn.stream(
227+
"GET",
228+
"https://example.com/",
229+
headers={"Connection": "upgrade", "Upgrade": "custom"},
230+
) as response:
231+
assert response.status == 101
232+
network_stream = response.extensions["network_stream"]
233+
content = network_stream.read(max_bytes=1024)
234+
assert content == b"..."

0 commit comments

Comments
 (0)