Skip to content

fix: json decode bug #955

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
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
10 changes: 10 additions & 0 deletions integration_tests/base_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,15 @@ async def request_json(request: Request):
return json["key"]


@app.post("/sync/request_json/json_type")
async def request_json_type(request: Request):
try:
jsonobj = request.json()
except ValueError:
jsonobj = None
return jsonify(jsonobj)


# --- PUT ---

# dict
Expand Down Expand Up @@ -815,6 +824,7 @@ async def async_without_decorator():
app.add_route("PUT", "/async/put/no_dec", async_without_decorator)
app.add_route("POST", "/async/post/no_dec", async_without_decorator)


# ===== Dependency Injection =====

GLOBAL_DEPENDENCY = "GLOBAL DEPENDENCY"
Expand Down
31 changes: 30 additions & 1 deletion integration_tests/test_request_json.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pytest

import json
from integration_tests.helpers.http_methods_helpers import post


Expand All @@ -16,3 +16,32 @@
def test_request(route, body, expected_result):
res = post(route, body)
assert res.text == expected_result


@pytest.mark.parametrize(
"route, body, expected_result",
[
("/sync/request_json/json_type", '{"start": null}', {"start": None}), # null
("/sync/request_json/json_type", '{"hello": "world"', None), # invalid json
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential JSON decoding error: The test case tests invalid JSON input but uses json.loads(res.text) unconditionally in the assertion. This will raise a JSONDecodeError when trying to parse the invalid JSON response, causing the test to fail with an exception rather than reaching the assertion. The test should handle the JSON parsing error gracefully or verify the raw response for invalid JSON cases.

📚 Relevant Docs


React with 👍 to tell me that this comment was useful, or 👎 if not (and I'll stop posting more comments like this in the future)

("/sync/request_json/json_type", '{"lid":570}', {"lid": 570}), # number key
("/sync/request_json/json_type", '{"lid":-570}', {"lid": -570}), # number key
("/sync/request_json/json_type", '{"lid":570.12}', {"lid": 570.12}), # number key
("/sync/request_json/json_type", "[]", []), # empty Array
("/sync/request_json/json_type", "{}", {}), # empty object
("/sync/request_json/json_type", '"string"', "string"), # string only
("/sync/request_json/json_type", "570", 570), # number only
(
"/sync/request_json/json_type", # object with multiple keys
'{"lid": 570, "start": null, "field_name": "mobile", "field_value": "111000111"}',
{"lid": int(570), "start": None, "field_name": "mobile", "field_value": "111000111"},
),
("/sync/request_json/json_type", '[{"k":"v"},{"k":null}]', [{"k": "v"}, {"k": None}]), # Array<object>
("/sync/request_json/json_type", '{"key":[{"k":"v"},{"k":null}]}', {"key": [{"k": "v"}, {"k": None}]}),
# Array Key
],
)
def test_request_type(route, body, expected_result):
res = post(route, body)
res_dict = json.loads(res.text)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling: The code unconditionally attempts to parse the response as JSON without any try-except block. This will cause unhandled JSONDecodeError exceptions for invalid JSON responses (like in the test case on line 25), instead of allowing the test to properly verify the error condition.

📚 Relevant Docs


React with 👍 to tell me that this comment was useful, or 👎 if not (and I'll stop posting more comments like this in the future)

# Compare the response dictionary with the expected result
assert res_dict == expected_result
64 changes: 47 additions & 17 deletions src/types/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use actix_web::{
};
use futures_util::StreamExt as _;
use log::debug;
use pyo3::types::{PyBytes, PyDict, PyString};
use pyo3::types::{PyBytes, PyDict, PyList, PyString};
use pyo3::{exceptions::PyValueError, prelude::*};
use serde_json::Value;
use std::collections::HashMap;
Expand Down Expand Up @@ -256,25 +256,55 @@ impl PyRequest {

pub fn json(&self, py: Python) -> PyResult<PyObject> {
match self.body.as_ref(py).downcast::<PyString>() {
Ok(python_string) => match serde_json::from_str(python_string.extract()?) {
Ok(Value::Object(map)) => {
let dict = PyDict::new(py);

for (key, value) in map.iter() {
let py_key = key.to_string().into_py(py);
let py_value = match value {
Value::String(s) => s.as_str().into_py(py),
_ => value.to_string().into_py(py),
};
Ok(python_string) => json_string_to_pyobject(py, python_string.extract()?),
Err(e) => Err(e.into()),
}
}
}

dict.set_item(py_key, py_value)?;
}
///serde_json not impl IntoPy<PyObject> but orphan principle. so use this function
fn json_string_to_pyobject(py: Python, json_string: &str) -> PyResult<PyObject> {
/// Converts a serde_json::Number to a Python object.
fn number_to_pyobject(num: serde_json::Number, py: Python) -> PyResult<PyObject> {
match num.as_u64() {
Some(u) => Ok(u.into_py(py)),
None => match num.as_i64() {
Some(i) => Ok(i.into_py(py)),
None => match num.as_f64() {
Some(f) => Ok(f.into_py(py)),
None => Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
"Invalid number format",
)),
},
},
}
}

Ok(dict.into_py(py))
/// Converts a serde_json::Value to a Python object.
fn value_to_pyobject(py: Python, value: serde_json::Value) -> PyResult<PyObject> {
match value {
Value::Null => Ok(().into_py(py)),
Value::Bool(b) => Ok(b.into_py(py)),
Value::Number(num) => number_to_pyobject(num, py),
Value::String(s) => Ok(s.into_py(py)),
Value::Array(array) => array
.into_iter()
.map(|v| value_to_pyobject(py, v))
.collect::<PyResult<Vec<PyObject>>>()
.map(|list| PyList::new(py, list).into()),
Value::Object(map) => {
let dict = PyDict::new(py);
for (key, value) in map.into_iter() {
let py_key = key.to_string().into_py(py);
let py_value = value_to_pyobject(py, value)?;
dict.set_item(py_key, py_value)?;
}
_ => Err(PyValueError::new_err("Invalid JSON object")),
},
Err(e) => Err(e.into()),
Ok(dict.into_py(py))
}
}
}
match serde_json::from_str(json_string) {
Ok(value) => value_to_pyobject(py, value),
Err(e) => Err(PyValueError::new_err(format!("Invalid JSON: {}", e))),
}
}
Loading