diff --git a/Cargo.lock b/Cargo.lock
index bd3f57ed0..1e2dccf7e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -10,7 +10,7 @@ checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5"
dependencies = [
"actix-rt",
"actix_derive",
- "bitflags",
+ "bitflags 1.3.2",
"bytes",
"crossbeam-channel",
"futures-core",
@@ -32,7 +32,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8"
dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
"bytes",
"futures-core",
"futures-sink",
@@ -54,7 +54,7 @@ dependencies = [
"actix-utils",
"actix-web",
"askama_escape",
- "bitflags",
+ "bitflags 1.3.2",
"bytes",
"derive_more",
"futures-core",
@@ -78,7 +78,7 @@ dependencies = [
"actix-utils",
"ahash 0.8.3",
"base64",
- "bitflags",
+ "bitflags 1.3.2",
"brotli",
"bytes",
"bytestring",
@@ -115,6 +115,44 @@ dependencies = [
"syn 2.0.27",
]
+[[package]]
+name = "actix-multipart"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b960e2aea75f49c8f069108063d12a48d329fc8b60b786dfc7552a9d5918d2d"
+dependencies = [
+ "actix-multipart-derive",
+ "actix-utils",
+ "actix-web",
+ "bytes",
+ "derive_more",
+ "futures-core",
+ "futures-util",
+ "httparse",
+ "local-waker",
+ "log",
+ "memchr",
+ "mime",
+ "serde",
+ "serde_json",
+ "serde_plain",
+ "tempfile",
+ "tokio",
+]
+
+[[package]]
+name = "actix-multipart-derive"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d"
+dependencies = [
+ "darling",
+ "parse-size",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.27",
+]
+
[[package]]
name = "actix-router"
version = "0.5.1"
@@ -374,6 +412,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+[[package]]
+name = "bitflags"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -498,6 +542,41 @@ dependencies = [
"typenum",
]
+[[package]]
+name = "darling"
+version = "0.20.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.27",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.27",
+]
+
[[package]]
name = "dashmap"
version = "5.5.0"
@@ -543,6 +622,22 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "errno"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
+
[[package]]
name = "flate2"
version = "1.0.26"
@@ -750,6 +845,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
[[package]]
name = "idna"
version = "0.4.0"
@@ -782,7 +883,7 @@ version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd1e1a01cfb924fd8c5c43b6827965db394f5a3a16c599ce03452266e1cf984c"
dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
"libc",
]
@@ -809,9 +910,15 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
[[package]]
name = "libc"
-version = "0.2.147"
+version = "0.2.153"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]]
name = "local-channel"
@@ -902,7 +1009,7 @@ dependencies = [
"libc",
"log",
"wasi",
- "windows-sys",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -950,9 +1057,15 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
- "windows-targets",
+ "windows-targets 0.48.1",
]
+[[package]]
+name = "parse-size"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae"
+
[[package]]
name = "paste"
version = "1.0.14"
@@ -1149,7 +1262,7 @@ version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
]
[[package]]
@@ -1188,6 +1301,7 @@ dependencies = [
"actix",
"actix-files",
"actix-http",
+ "actix-multipart",
"actix-web",
"actix-web-actors",
"anyhow",
@@ -1223,6 +1337,19 @@ dependencies = [
"semver",
]
+[[package]]
+name = "rustix"
+version = "0.38.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
+dependencies = [
+ "bitflags 2.5.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "ryu"
version = "1.0.15"
@@ -1264,6 +1391,15 @@ dependencies = [
"serde",
]
+[[package]]
+name = "serde_plain"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -1328,9 +1464,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
dependencies = [
"libc",
- "windows-sys",
+ "windows-sys 0.48.0",
]
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
[[package]]
name = "syn"
version = "1.0.109"
@@ -1359,6 +1501,18 @@ version = "0.12.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2faeef5759ab89935255b1a4cd98e0baf99d1085e37d36599c625dac49ae8e"
+[[package]]
+name = "tempfile"
+version = "3.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "time"
version = "0.3.23"
@@ -1418,7 +1572,7 @@ dependencies = [
"signal-hook-registry",
"socket2 0.4.9",
"tokio-macros",
- "windows-sys",
+ "windows-sys 0.48.0",
]
[[package]]
@@ -1584,7 +1738,16 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
- "windows-targets",
+ "windows-targets 0.48.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.4",
]
[[package]]
@@ -1593,13 +1756,28 @@ version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.4",
+ "windows_aarch64_msvc 0.52.4",
+ "windows_i686_gnu 0.52.4",
+ "windows_i686_msvc 0.52.4",
+ "windows_x86_64_gnu 0.52.4",
+ "windows_x86_64_gnullvm 0.52.4",
+ "windows_x86_64_msvc 0.52.4",
]
[[package]]
@@ -1608,42 +1786,84 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
+
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
+
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
+
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
+
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
+
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
+
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
+
[[package]]
name = "zstd"
version = "0.12.4"
diff --git a/Cargo.toml b/Cargo.toml
index 2078ccc05..fa81bf9d2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -35,6 +35,7 @@ pythonize = "0.19.0"
serde = "1.0.178"
serde_json = "1.0.104"
once_cell = "1.8.0"
+actix-multipart = "0.6.1"
[features]
io-uring = ["actix-web/experimental-io-uring"]
diff --git a/README.md b/README.md
index 5be052088..9bce73def 100644
--- a/README.md
+++ b/README.md
@@ -116,6 +116,7 @@ python --version
- Multi Core Scaling
- WebSockets!
- Middlewares
+- Built in form data handling
- Dependency Injection
- Hot Reloading
- Direct Rust Integration
diff --git a/docs_src/src/components/documentation/ApiDocs.jsx b/docs_src/src/components/documentation/ApiDocs.jsx
index 008f255d0..cb055bee9 100644
--- a/docs_src/src/components/documentation/ApiDocs.jsx
+++ b/docs_src/src/components/documentation/ApiDocs.jsx
@@ -53,6 +53,11 @@ const guides = [
description:
'Learn how to upload and download files to your server using Robyn.',
},
+ {
+ href: '/documentation/api_reference/form_data',
+ name: 'Form Data',
+ description: 'Learn how to handle form data.',
+ },
{
href: '/documentation/api_reference/websockets',
name: 'Websockets',
diff --git a/docs_src/src/components/documentation/Navigation.jsx b/docs_src/src/components/documentation/Navigation.jsx
index 0e27227b1..8805197c5 100644
--- a/docs_src/src/components/documentation/Navigation.jsx
+++ b/docs_src/src/components/documentation/Navigation.jsx
@@ -260,6 +260,10 @@ export const navigation = [
href: '/documentation/api_reference/file-uploads',
title: 'File Uploads',
},
+ {
+ href: '/documentation/api_reference/form_data',
+ title: 'Form Data',
+ },
{
href: '/documentation/api_reference/websockets',
title: 'Websockets',
diff --git a/docs_src/src/pages/documentation/api_reference/file-uploads.mdx b/docs_src/src/pages/documentation/api_reference/file-uploads.mdx
index 2fdd0265b..dec4aa926 100644
--- a/docs_src/src/pages/documentation/api_reference/file-uploads.mdx
+++ b/docs_src/src/pages/documentation/api_reference/file-uploads.mdx
@@ -6,7 +6,7 @@ export const description =
Batman learned how to handle file uploads using Robyn. He created an endpoint to handle file uploads using the following code:
-## Scaling the Application
+## Sending a File without MultiPart Form Data
@@ -17,8 +17,6 @@ Batman scaled his application across multiple cores for better performance. He u
```python {{ title: 'untyped' }}
- from robyn.robyn import File
-
@app.post("/upload")
async def upload():
body = request.body
@@ -31,8 +29,6 @@ Batman scaled his application across multiple cores for better performance. He u
return {'message': 'success'}
```
```python {{ title: 'typed' }}
- from robyn.robyn import File
-
@app.post("/upload")
async def upload():
body = request.body
@@ -49,6 +45,44 @@ Batman scaled his application across multiple cores for better performance. He u
+
+## Sending a File with MultiPart Form Data
+
+
+
+Batman scaled his application across multiple cores for better performance. He used the following command:
+
+
+
+
+
+ ```python {{ title: 'untyped' }}
+ @app.post("/upload")
+ async def upload(request):
+ file = request.files['filename']
+
+ # write whatever filename
+ with open('filename', 'wb') as f:
+ f.write(file)
+
+ return {'message': 'success'}
+ ```
+ ```python {{ title: 'typed' }}
+ @app.post("/upload")
+ async def upload(request: Request):
+ file = request.files['filename']
+
+ # write whatever filename
+ with open('filename', 'wb') as f:
+ f.write(file)
+
+ return {'message': 'success'}
+ ```
+
+
+
+
+
---
## File Downloads
@@ -186,9 +220,9 @@ Batman scaled his application across multiple cores for better performance. He u
## What's next?
-Now, Batman was ready to learn about the advanced features of Robyn. He wanted to find a way to get realtime updates in his dashboard.
+Now, Batman was ready to learn about the advanced features of Robyn. He wanted to find a way to handle form data
-- [WebSockets](/documentation/api_reference/websockets)
+- [Form Data](/documentation/api_reference/form_data)
diff --git a/docs_src/src/pages/documentation/api_reference/form_data.mdx b/docs_src/src/pages/documentation/api_reference/form_data.mdx
new file mode 100644
index 000000000..fe06179bf
--- /dev/null
+++ b/docs_src/src/pages/documentation/api_reference/form_data.mdx
@@ -0,0 +1,43 @@
+export const description =
+ 'On this page, we’ll dive into using the form data.'
+
+## Fom Data
+
+Batman learned how to handle file uploads using Robyn. Now, he wanted to handle the form data
+
+
+## Handling Form Data
+
+
+
+Batman uploaded some multipart form data and wanted to handle it using the following code:
+
+
+
+
+
+ ```python {{ title: 'untyped' }}
+ @app.post("/upload")
+ async def upload(request):
+ form_data = request.from_data
+
+ return form_data
+ ```
+ ```python {{ title: 'typed' }}
+ @app.post("/upload")
+ async def upload(request):
+ form_data = request.from_data
+
+ return form_data
+ ```
+
+
+
+
+
+
+## What's next?
+
+Now, Batman was ready to learn about the advanced features of Robyn. He wanted to find a way to get realtime updates in his dashboard.
+
+- [WebSockets](/documentation/api_reference/websockets)
diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py
index 9bc841ed2..b87ed139d 100644
--- a/integration_tests/base_routes.py
+++ b/integration_tests/base_routes.py
@@ -460,6 +460,17 @@ async def file_download_async():
return serve_file(file_path)
+# Multipart file
+
+
+@app.post("/sync/multipart-file")
+def sync_multipart_file(request: Request):
+ file = request.files["test.png"]
+ with open("test.png", "wb") as f:
+ f.write(file)
+ return "File uploaded"
+
+
# Queries
@@ -548,6 +559,11 @@ async def async_body_post(request: Request):
return request.body
+@app.post("/sync/form_data")
+def sync_form_data(request: Request):
+ return request.form_data
+
+
# JSON Request
@@ -631,6 +647,7 @@ async def async_dict_delete():
@app.delete("/sync/body")
def sync_body_delete(request: Request):
+ print(request.body)
return request.body
diff --git a/integration_tests/helpers/http_methods_helpers.py b/integration_tests/helpers/http_methods_helpers.py
index 921375e72..b78fecfaa 100644
--- a/integration_tests/helpers/http_methods_helpers.py
+++ b/integration_tests/helpers/http_methods_helpers.py
@@ -11,10 +11,10 @@ def check_response(response: requests.Response, expected_status_code: int):
headers is not present in the response.
"""
assert response.status_code == expected_status_code
- print(response.headers)
assert response.headers.get("global_after") == "global_after_request"
assert "server" in response.headers
assert response.headers.get("server") == "robyn"
+ print("This is the response text", response)
def get(
diff --git a/robyn/robyn.pyi b/robyn/robyn.pyi
index d5b7b916d..383adf8dd 100644
--- a/robyn/robyn.pyi
+++ b/robyn/robyn.pyi
@@ -246,10 +246,13 @@ class Request:
Attributes:
query_params (QueryParams): The query parameters of the request. e.g. /user?id=123 -> {"id": "123"}
headers Headers: The headers of the request. e.g. Headers({"Content-Type": "application/json"})
- params (dict[str, str]): The parameters of the request. e.g. /user/:id -> {"id": "123"}
+ path_params (dict[str, str]): The parameters of the request. e.g. /user/:id -> {"id": "123"}
body (Union[str, bytes]): The body of the request. If the request is a JSON, it will be a dict.
- method (str): The method of the request. e.g. GET, POST, PUT, DELETE
+ url (Url): The url of the request. e.g. https://localhost/user
+ form_data (dict[str, str]): The form data of the request. e.g. {"name": "John"}
+ files (dict[str, bytes]): The files of the request. e.g. {"file": b"file"}
ip_addr (Optional[str]): The IP Address of the client
+ identity (Optional[Identity]): The identity of the client
"""
query_params: QueryParams
@@ -258,6 +261,8 @@ class Request:
body: Union[str, bytes]
method: str
url: Url
+ form_data: dict[str, str]
+ files: dict[str, bytes]
ip_addr: Optional[str]
identity: Optional[Identity]
diff --git a/src/server.rs b/src/server.rs
index bd52db435..28f49ec25 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -23,7 +23,6 @@ use std::{env, thread};
use actix_files::Files;
use actix_http::KeepAlive;
-use actix_web::web::Bytes;
use actix_web::*;
// pyO3 module
@@ -190,18 +189,18 @@ impl Server {
move |router: web::Data>,
const_router: web::Data>,
middleware_router: web::Data>,
+ payload: web::Payload,
global_request_headers,
global_response_headers,
- body,
req| {
pyo3_asyncio::tokio::scope_local(task_locals.clone(), async move {
index(
router,
+ payload,
const_router,
middleware_router,
global_request_headers,
global_response_headers,
- body,
req,
)
.await
@@ -366,14 +365,14 @@ impl Default for Server {
/// path, and returns a Future of a Response.
async fn index(
router: web::Data>,
+ payload: web::Payload,
const_router: web::Data>,
middleware_router: web::Data>,
global_request_headers: web::Data>,
global_response_headers: web::Data>,
- body: Bytes,
req: HttpRequest,
) -> impl Responder {
- let mut request = Request::from_actix_request(&req, body, &global_request_headers);
+ let mut request = Request::from_actix_request(&req, payload, &global_request_headers).await;
// Before middleware
// Global
diff --git a/src/types/headers.rs b/src/types/headers.rs
index a388d08bc..129477af1 100644
--- a/src/types/headers.rs
+++ b/src/types/headers.rs
@@ -23,13 +23,12 @@ impl Headers {
let new_value = value.downcast::();
- if new_value.is_err() {
+ if let Ok(new_value) = new_value {
+ let value: Vec = new_value.iter().map(|x| x.to_string()).collect();
+ headers.entry(key).or_insert_with(Vec::new).extend(value);
+ } else {
let value = value.to_string();
headers.entry(key).or_insert_with(Vec::new).push(value);
- } else {
- let value: Vec =
- new_value.unwrap().iter().map(|x| x.to_string()).collect();
- headers.entry(key).or_insert_with(Vec::new).extend(value);
}
}
Headers { headers }
@@ -100,12 +99,12 @@ impl Headers {
let key = key.to_string().to_lowercase();
let new_value = value.downcast::();
- if new_value.is_err() {
+ if let Ok(new_value) = new_value {
+ let value: Vec = new_value.iter().map(|x| x.to_string()).collect();
+ self.headers.entry(key).or_default().extend(value);
+ } else {
let value = value.to_string();
self.headers.entry(key).or_default().push(value);
- } else {
- let value: Vec = new_value.unwrap().iter().map(|x| x.to_string()).collect();
- self.headers.entry(key).or_default().extend(value);
}
}
}
diff --git a/src/types/request.rs b/src/types/request.rs
index 98ed788d6..78b2cffcf 100644
--- a/src/types/request.rs
+++ b/src/types/request.rs
@@ -1,5 +1,12 @@
-use actix_web::{web::Bytes, HttpRequest};
-use pyo3::{exceptions::PyValueError, prelude::*, types::PyDict, types::PyString};
+use actix_multipart::Multipart;
+use actix_web::{
+ web::{self, BytesMut},
+ Error, HttpRequest,
+};
+use futures_util::StreamExt as _;
+use log::debug;
+use pyo3::types::{PyBytes, PyDict, PyString};
+use pyo3::{exceptions::PyValueError, prelude::*};
use serde_json::Value;
use std::collections::HashMap;
@@ -19,6 +26,8 @@ pub struct Request {
pub url: Url,
pub ip_addr: Option,
pub identity: Option,
+ pub form_data: Option>,
+ pub files: Option>>,
}
impl ToPyObject for Request {
@@ -30,6 +39,28 @@ impl ToPyObject for Request {
Ok(s) => s.into_py(py),
Err(_) => self.body.clone().into_py(py),
};
+ let form_data: Py = match &self.form_data {
+ Some(data) => {
+ let dict = PyDict::new(py);
+ for (key, value) in data.iter() {
+ dict.set_item(key, value).unwrap();
+ }
+ dict.into_py(py)
+ }
+ None => PyDict::new(py).into_py(py),
+ };
+
+ let files: Py = match &self.files {
+ Some(data) => {
+ let dict = PyDict::new(py);
+ for (key, value) in data.iter() {
+ let bytes = PyBytes::new(py, value);
+ dict.set_item(key, bytes).unwrap();
+ }
+ dict.into_py(py)
+ }
+ None => PyDict::new(py).into_py(py),
+ };
let request = PyRequest {
query_params,
@@ -40,14 +71,58 @@ impl ToPyObject for Request {
url: self.url.clone(),
ip_addr: self.ip_addr.clone(),
identity: self.identity.clone(),
+ form_data: form_data.clone(),
+ files: files.clone(),
};
Py::new(py, request).unwrap().as_ref(py).into()
}
}
+async fn handle_multipart(
+ mut payload: Multipart,
+ files: &mut HashMap>,
+ form_data: &mut HashMap,
+ body: &mut Vec,
+) -> Result<(), Error> {
+ // Iterate over multipart stream
+
+ while let Some(item) = payload.next().await {
+ let mut field = item?;
+
+ let mut data = Vec::new();
+ // Read the field data
+ while let Some(chunk) = field.next().await {
+ debug!("Chunk: {:?}", chunk);
+ let data_chunk = chunk?;
+ data.extend_from_slice(&data_chunk);
+ }
+
+ let content_disposition = field.content_disposition();
+ let field_name = content_disposition.get_name().unwrap_or_default();
+ let file_name = content_disposition.get_filename().map(|s| s.to_string());
+
+ body.extend_from_slice(&data.clone());
+
+ if let Some(name) = file_name {
+ files.insert(name, data);
+ } else if let Ok(text) = String::from_utf8(data) {
+ form_data.insert(field_name.to_string(), text);
+ }
+ }
+
+ Ok(())
+}
+
impl Request {
- pub fn from_actix_request(req: &HttpRequest, body: Bytes, global_headers: &Headers) -> Self {
+ pub async fn from_actix_request(
+ req: &HttpRequest,
+ mut payload: web::Payload,
+ global_headers: &Headers,
+ ) -> Self {
let mut query_params: QueryParams = QueryParams::new();
+ let mut form_data: HashMap = HashMap::new();
+ let mut files = HashMap::new();
+
if !req.query_string().is_empty() {
let split = req.query_string().split('&');
for s in split {
@@ -60,8 +135,41 @@ impl Request {
}
let mut headers = Headers::from_actix_headers(req.headers());
+ debug!("Global headers: {:?}", global_headers);
headers.extend(global_headers);
+ let body: Vec = if headers.contains(String::from("content-type"))
+ && headers
+ .get(String::from("content-type"))
+ .is_ok_and(|val| val == "multipart/form-data")
+ {
+ let h = headers.get(String::from("content-type")).unwrap();
+ debug!("Content-Type: {:?}", h);
+ let multipart = Multipart::new(req.headers(), payload);
+ let mut body_local: Vec = Vec::new();
+
+ let a = handle_multipart(multipart, &mut files, &mut form_data, &mut body_local).await;
+
+ if let Err(e) = a {
+ debug!("Error handling multipart data: {:?}", e);
+ }
+
+ body_local
+ } else {
+ let mut body_local = BytesMut::new();
+ while let Some(chunk) = payload.next().await {
+ let chunk = chunk.expect("Failed to read chunk from payload");
+ body_local.extend_from_slice(&chunk);
+ }
+ body_local.freeze().to_vec()
+ };
+
+ debug!("Request body: {:?}", body);
+ debug!("Request headers: {:?}", headers);
+ debug!("Request query params: {:?}", query_params);
+ debug!("Request form data: {:?}", form_data);
+ debug!("Request files: {:?}", files);
+
let url = Url::new(
req.connection_info().scheme(),
req.connection_info().host(),
@@ -74,10 +182,12 @@ impl Request {
headers,
method: req.method().as_str().to_owned(),
path_params: HashMap::new(),
- body: body.to_vec(),
+ body,
url,
ip_addr,
identity: None,
+ form_data: Some(form_data),
+ files: Some(files),
}
}
}
@@ -101,6 +211,10 @@ pub struct PyRequest {
pub url: Url,
#[pyo3(get)]
pub ip_addr: Option,
+ #[pyo3(get, set)]
+ pub form_data: Py,
+ #[pyo3(get, set)]
+ pub files: Py,
}
#[pymethods]
@@ -114,6 +228,8 @@ impl PyRequest {
body: Py,
method: String,
url: Url,
+ form_data: Py,
+ files: Py,
identity: Option,
ip_addr: Option,
) -> Self {
@@ -125,6 +241,8 @@ impl PyRequest {
body,
method,
url,
+ form_data,
+ files,
ip_addr,
}
}
diff --git a/unit_tests/test_request_object.py b/unit_tests/test_request_object.py
index f1f46cd88..aa3f7a117 100644
--- a/unit_tests/test_request_object.py
+++ b/unit_tests/test_request_object.py
@@ -16,6 +16,8 @@ def test_request_object():
url=url,
ip_addr=None,
identity=None,
+ form_data={},
+ files={},
)
assert request.url.scheme == "https"