Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 35 additions & 35 deletions src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,41 +238,6 @@ pub async fn run_rsgi(
return send_handler_map(&protocol, is_head, mapped).await;
}
}
let read_fut = Python::with_gil(|py| {
let p = protocol.bind(py);
let aw: Bound<PyAny> = p.call0()?;
pyo3_async_runtimes::tokio::into_future(aw)
})?;
let body_obj: PyObject = read_fut.await?;
let body_bytes: Vec<u8> = Python::with_gil(|py| -> PyResult<Vec<u8>> {
let b = body_obj.bind(py);
if let Ok(x) = b.extract::<Vec<u8>>() {
return Ok(x);
}
if let Ok(s) = b.str() {
return Ok(s.to_string().into_bytes());
}
Ok(Vec::new())
})?;
let max = form::max_body_bytes();
if (body_bytes.len() as u64) > max {
return response::send_text(
&protocol,
413,
r#"{"error":"payload too large"}"#,
"application/json; charset=utf-8",
)
.await;
}
let (auth, cookie_raw) =
Python::with_gil(|py| -> PyResult<(Option<String>, Option<String>)> {
let s = scope.bind(py);
let headers = s.getattr("headers")?;
Ok((
header_get_lax(&headers, "authorization"),
header_get_lax(&headers, "cookie"),
))
})?;
let route_out: Option<(usize, HashMap<String, String>)> = {
let st = state.read();
match match_route(&st, &method, &path) {
Expand Down Expand Up @@ -343,6 +308,41 @@ pub async fn run_rsgi(
e.handler_varkw,
))
})?;
let read_fut = Python::with_gil(|py| {
let p = protocol.bind(py);
let aw: Bound<PyAny> = p.call0()?;
pyo3_async_runtimes::tokio::into_future(aw)
})?;
let body_obj: PyObject = read_fut.await?;
let body_bytes: Vec<u8> = Python::with_gil(|py| -> PyResult<Vec<u8>> {
let b = body_obj.bind(py);
if let Ok(x) = b.extract::<Vec<u8>>() {
return Ok(x);
}
if let Ok(s) = b.str() {
return Ok(s.to_string().into_bytes());
}
Ok(Vec::new())
})?;
let max = form::max_body_bytes();
if (body_bytes.len() as u64) > max {
return response::send_text(
&protocol,
413,
r#"{"error":"payload too large"}"#,
"application/json; charset=utf-8",
)
.await;
}
let (auth, cookie_raw) =
Python::with_gil(|py| -> PyResult<(Option<String>, Option<String>)> {
let s = scope.bind(py);
let headers = s.getattr("headers")?;
Ok((
header_get_lax(&headers, "authorization"),
header_get_lax(&headers, "cookie"),
))
})?;
let mut claims_val: Option<JsonValue> = None;
if require_jwt {
let key = match jwt_secret {
Expand Down
72 changes: 72 additions & 0 deletions tests/test_dispatch_fast_fail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Issue #71: 404/405 should fail before request body read on RSGI path."""

from __future__ import annotations

import asyncio
from types import SimpleNamespace
from typing import Any

from oxyroute import App


class _ProbeProtocol:
def __init__(self) -> None:
self.body_calls = 0
self.sent: list[tuple[str, int, Any]] = []

async def __call__(self) -> bytes:
self.body_calls += 1
return b'{"large":"payload"}'

def response_str(self, status: int, _headers: list, body: str) -> None:
self.sent.append(("str", int(status), body))

def response_bytes(self, status: int, _headers: list, body: bytes) -> None:
self.sent.append(("bytes", int(status), body))

def response_empty(self, status: int, _headers: list) -> None:
self.sent.append(("empty", int(status), b""))


def _scope(method: str, path: str) -> Any:
return SimpleNamespace(
proto="http",
method=method,
path=path,
query_string="",
headers={},
)


def test_rsgi_404_does_not_consume_body() -> None:
app = App()

@app.get("/exists")
def exists() -> str:
return "ok"

async def _run() -> None:
proto = _ProbeProtocol()
await app.__rsgi__(_scope("POST", "/missing"), proto)
assert proto.body_calls == 0
assert proto.sent
assert proto.sent[-1][1] == 404

asyncio.run(_run())


def test_rsgi_405_does_not_consume_body() -> None:
app = App()

@app.post("/x")
def x() -> str:
return "ok"

async def _run() -> None:
proto = _ProbeProtocol()
await app.__rsgi__(_scope("GET", "/x"), proto)
assert proto.body_calls == 0
assert proto.sent
assert proto.sent[-1][1] == 405

asyncio.run(_run())
Loading