From 8c8ac0889eb545ce643fd77dc76be2b62094c29d Mon Sep 17 00:00:00 2001 From: Jason Washburn Date: Sun, 16 Apr 2023 01:51:27 +0000 Subject: [PATCH 1/7] change headers to python dict Signed-off-by: Jason Washburn --- crates/spin-python-engine/src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/spin-python-engine/src/lib.rs b/crates/spin-python-engine/src/lib.rs index 6f2f6a3..f09a87a 100644 --- a/crates/spin-python-engine/src/lib.rs +++ b/crates/spin-python-engine/src/lib.rs @@ -16,7 +16,7 @@ use { key_value, outbound_http, redis::{self, RedisParameter, RedisResult}, }, - std::{env, ops::Deref, str, sync::Arc}, + std::{collections::HashMap, env, ops::Deref, str, sync::Arc}, }; thread_local! { @@ -44,7 +44,7 @@ struct HttpRequest { #[pyo3(get, set)] uri: String, #[pyo3(get, set)] - headers: Vec<(String, String)>, + headers: HashMap, #[pyo3(get, set)] body: Option>, } @@ -55,7 +55,7 @@ impl HttpRequest { fn new( method: String, uri: String, - headers: Vec<(String, String)>, + headers: HashMap, body: Option>, ) -> Self { Self { @@ -74,7 +74,7 @@ struct HttpResponse { #[pyo3(get, set)] status: u16, #[pyo3(get, set)] - headers: Vec<(String, String)>, + headers: HashMap, #[pyo3(get, set)] body: Option>, } @@ -82,7 +82,7 @@ struct HttpResponse { #[pyo3::pymethods] impl HttpResponse { #[new] - fn new(status: u16, headers: Vec<(String, String)>, body: Option>) -> Self { + fn new(status: u16, headers: HashMap, body: Option>) -> Self { Self { status, headers, @@ -179,7 +179,7 @@ fn http_send(module: &PyModule, request: HttpRequest) -> PyResult .to_owned(), )) }) - .collect::>()?, + .collect::>>()?, body: response .into_body() .as_deref() @@ -404,7 +404,7 @@ fn handle(request: Request) -> Result { .to_owned(), )) }) - .collect::>()?, + .collect::>>()?, body: request .body() .as_deref() From ae34656e2b61c29e1aba60ea39069a29fbb8e3aa Mon Sep 17 00:00:00 2001 From: Jason Washburn Date: Sun, 16 Apr 2023 01:54:11 +0000 Subject: [PATCH 2/7] update examples for dict headers Signed-off-by: Jason Washburn --- examples/KV/app.py | 12 ++++++------ examples/external_lib/app.py | 2 +- examples/hello_world/app.py | 2 +- examples/outbound_http/app.py | 4 ++-- examples/outbound_redis/app.py | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/KV/app.py b/examples/KV/app.py index a2757aa..9d9ffba 100644 --- a/examples/KV/app.py +++ b/examples/KV/app.py @@ -13,16 +13,16 @@ def handle_request(request): status = 200 else: status = 404 - return Response(status, [("content-type", "text/plain")], value) + return Response(status, {"content-type": "text/plain"}, value) case "POST": store.set(request.uri, request.body) - return Response(200, [("content-type", "text/plain")]) + return Response(200, {"content-type": "text/plain"}) case "DELETE": store.delete(request.uri) - return Response(200, [("content-type", "text/plain")]) + return Response(200, {"content-type": "text/plain"}) case "HEAD": if store.exists(request.uri): - return Response(200, [("content-type", "text/plain")]) - return Response(404, [("content-type", "text/plain")]) + return Response(200, {"content-type": "text/plain"}) + return Response(404, {"content-type": "text/plain"}) case default: - return Response(405, [("content-type", "text/plain")]) + return Response(405, {"content-type": "text/plain"}) diff --git a/examples/external_lib/app.py b/examples/external_lib/app.py index 2542dad..e3ceebf 100644 --- a/examples/external_lib/app.py +++ b/examples/external_lib/app.py @@ -11,5 +11,5 @@ def handle_request(request): """ return Response(200, - [("content-type", "text/plain")], + {"content-type": "text/plain"}, bytes(f"Toml content:{toml.loads(some_toml)}", "utf-8")) diff --git a/examples/hello_world/app.py b/examples/hello_world/app.py index 0dffcf1..53c9a90 100644 --- a/examples/hello_world/app.py +++ b/examples/hello_world/app.py @@ -4,5 +4,5 @@ def handle_request(request): return Response(200, - [("content-type", "text/plain")], + {"content-type": "text/plain"}, bytes(f"Hello from Python!", "utf-8")) diff --git a/examples/outbound_http/app.py b/examples/outbound_http/app.py index 7831b06..3999609 100644 --- a/examples/outbound_http/app.py +++ b/examples/outbound_http/app.py @@ -4,8 +4,8 @@ def handle_request(request): response = http_send( - Request("GET", "https://some-random-api.ml/facts/dog", [], None)) + Request("GET", "https://some-random-api.ml/facts/dog", {}, None)) return Response(200, - [("content-type", "text/plain")], + {"content-type": "text/plain"}, bytes(f"Here is a dog fact: {str(response.body, 'utf-8')}", "utf-8")) diff --git a/examples/outbound_redis/app.py b/examples/outbound_redis/app.py index 75f0945..c32a1ae 100644 --- a/examples/outbound_redis/app.py +++ b/examples/outbound_redis/app.py @@ -20,5 +20,5 @@ def handle_request(request): assert value == b"bar", f"expected \"bar\", got \"{str(value, 'utf-8')}\"" return Response(200, - [("content-type", "text/plain")], + {"content-type": "text/plain"}, bytes(f"Executed outbound Redis commands", "utf-8")) From d88ed0cb187e671ca83d0907184b5d16443f3fa8 Mon Sep 17 00:00:00 2001 From: Jason Washburn Date: Sun, 16 Apr 2023 01:54:58 +0000 Subject: [PATCH 3/7] update template and test app for dict headers Signed-off-by: Jason Washburn --- templates/http-py/content/app.py | 2 +- test/test-app/app.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/http-py/content/app.py b/templates/http-py/content/app.py index 3dd931f..37da2b0 100644 --- a/templates/http-py/content/app.py +++ b/templates/http-py/content/app.py @@ -4,5 +4,5 @@ def handle_request(request): return Response(200, - [("content-type", "text/plain")], + {"content-type": "text/plain"}, bytes(f"Hello from the Python SDK", "utf-8")) diff --git a/test/test-app/app.py b/test/test-app/app.py index 769e3f4..7e61d78 100644 --- a/test/test-app/app.py +++ b/test/test-app/app.py @@ -7,7 +7,7 @@ def handle_request(request): if request.uri == "/foo": return Response(200, - [("content-type", "text/plain")], + {"content-type": "text/plain"}, bytes(f"foo indeed", "utf-8")) print(f"Got request URI: {request.uri}") @@ -26,9 +26,9 @@ def handle_request(request): my_file = open("/foo.txt", "r") print(f"And here's the content of foo.txt: {my_file.read()}") - response = http_send(Request("GET", "http://localhost:3000/foo", [], None)) + response = http_send(Request("GET", "http://localhost:3000/foo", {}, None)) print(f"Got foo: {str(response.body, 'utf-8')}") return Response(200, - [("content-type", "text/plain")], + {"content-type": "text/plain"}, bytes(f"Hello from Python! Got request URI: {request.uri}", "utf-8")) From a18ab505beec61c98dde5cb136394d9fe62c059f Mon Sep 17 00:00:00 2001 From: Jason Washburn Date: Tue, 25 Apr 2023 01:53:28 +0000 Subject: [PATCH 4/7] adds parsing of repeated header names Signed-off-by: Jason Washburn --- crates/spin-python-engine/src/lib.rs | 29 ++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/spin-python-engine/src/lib.rs b/crates/spin-python-engine/src/lib.rs index f09a87a..e9adc07 100644 --- a/crates/spin-python-engine/src/lib.rs +++ b/crates/spin-python-engine/src/lib.rs @@ -393,18 +393,23 @@ fn handle(request: Request) -> Result { let request = HttpRequest { method: request.method().as_str().to_owned(), uri, - headers: request - .headers() - .iter() - .map(|(k, v)| { - Ok(( - k.as_str().to_owned(), - str::from_utf8(v.as_bytes()) - .map_err(Anyhow::from)? - .to_owned(), - )) - }) - .collect::>>()?, + headers: request.headers().iter().fold( + HashMap::new(), + |mut acc: HashMap, (k, v): (&HeaderName, &HeaderValue)| { + let key = k.as_str().to_owned(); + let value = str::from_utf8(v.as_bytes()) + //.map_err(Anyhow::from) + .unwrap() + .to_owned(); + acc.entry(key) + .and_modify(|existing_value| { + existing_value.push(','); + existing_value.push_str(&value); + }) + .or_insert(value); + acc + }, + ), body: request .body() .as_deref() From 521a822cd3a256ecbddd74532f1fe6cfa128f153 Mon Sep 17 00:00:00 2001 From: Jason Washburn Date: Tue, 25 Apr 2023 10:26:33 +0000 Subject: [PATCH 5/7] adds error handling to header parsing Signed-off-by: Jason Washburn --- crates/spin-python-engine/src/lib.rs | 37 +++++++++++++++------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/crates/spin-python-engine/src/lib.rs b/crates/spin-python-engine/src/lib.rs index e9adc07..c36bc7e 100644 --- a/crates/spin-python-engine/src/lib.rs +++ b/crates/spin-python-engine/src/lib.rs @@ -393,23 +393,26 @@ fn handle(request: Request) -> Result { let request = HttpRequest { method: request.method().as_str().to_owned(), uri, - headers: request.headers().iter().fold( - HashMap::new(), - |mut acc: HashMap, (k, v): (&HeaderName, &HeaderValue)| { - let key = k.as_str().to_owned(); - let value = str::from_utf8(v.as_bytes()) - //.map_err(Anyhow::from) - .unwrap() - .to_owned(); - acc.entry(key) - .and_modify(|existing_value| { - existing_value.push(','); - existing_value.push_str(&value); - }) - .or_insert(value); - acc - }, - ), + headers: request + .headers() + .iter() + .try_fold::<_, _, PyResult>>( + HashMap::new(), + |mut acc: HashMap, (k, v): (&HeaderName, &HeaderValue)| { + let key = k.as_str().to_owned(); + let value = str::from_utf8(v.as_bytes()) + .map_err(Anyhow::from)? + .to_owned(); + acc.entry(key) + .and_modify(|existing_value| { + existing_value.push(','); + existing_value.push_str(&value); + }) + .or_insert(value); + Ok(acc) + }, + ) + .map_err(Anyhow::from)?, body: request .body() .as_deref() From 323063874bd308e65ecb4fa45066a6861e0953b5 Mon Sep 17 00:00:00 2001 From: Jason Washburn Date: Tue, 25 Apr 2023 11:01:30 +0000 Subject: [PATCH 6/7] adds space between multiple header values Signed-off-by: Jason Washburn --- crates/spin-python-engine/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/spin-python-engine/src/lib.rs b/crates/spin-python-engine/src/lib.rs index c36bc7e..5cc8b4a 100644 --- a/crates/spin-python-engine/src/lib.rs +++ b/crates/spin-python-engine/src/lib.rs @@ -405,7 +405,7 @@ fn handle(request: Request) -> Result { .to_owned(); acc.entry(key) .and_modify(|existing_value| { - existing_value.push(','); + existing_value.push_str(", "); existing_value.push_str(&value); }) .or_insert(value); From 6984b7d14a75531654231cf9eb33bab2c7cb5a48 Mon Sep 17 00:00:00 2001 From: Jason Washburn Date: Wed, 26 Apr 2023 09:51:58 +0000 Subject: [PATCH 7/7] adds duplicate header names test Signed-off-by: Jason Washburn --- .github/workflows/test.yaml | 2 +- test/test-app/app.py | 33 +++++++++++++++++++++++++++++++++ test/test.sh | 3 ++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a5ee7fa..a428b29 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -83,7 +83,7 @@ jobs: uses: engineerd/configurator@v0.0.8 with: name: "spin" - url: "https://github.com/fermyon/spin/releases/download/v0.9.0/spin-v0.9.0-linux-amd64.tar.gz" + url: "https://github.com/fermyon/spin/releases/download/canary/spin-canary-linux-amd64.tar.gz" pathInArchive: "spin" - name: Install pipenv diff --git a/test/test-app/app.py b/test/test-app/app.py index 7e61d78..910d5e5 100644 --- a/test/test-app/app.py +++ b/test/test-app/app.py @@ -4,12 +4,45 @@ from os import environ import toml + def handle_request(request): if request.uri == "/foo": return Response(200, {"content-type": "text/plain"}, bytes(f"foo indeed", "utf-8")) + if request.uri == "/duplicateheadertest": + test_headers = [("spin-header-test-key1", "value1"), ("spin-header-test-key2", "value2"), + ("spin-header-test-key1", "value3"), ("spin-header-test-key2", "value4")] + + key1_test_passes = request.headers.get( + "spin-header-test-key1") == "value1, value3" + key2_test_passes = request.headers.get( + "spin-header-test-key2") == "value2, value4" + if key1_test_passes and key2_test_passes: + response_content = "Duplicate Header Name Test: Pass" + response_code = 200 + + else: + example_curl_headers = '-H "spin-header-test-key1: value1" -H "spin-header-test-key2: value2" -H "spin-header-test-key1: value3" -H "spin-header-test-key2: value4"' + example_curl_request = f'curl {example_curl_headers} http://127.0.0.1:3000/duplicateheadertest' + required_headers = "\n".join( + [str(header) for header in test_headers]) + response_content = f""" +---------------------- Duplicate Header Name Test ------------------------------------- +To make this test pass, you must include the following headers in your request: +{required_headers} + +Example Passing Curl Request: {example_curl_request} + +Actual Headers Received: +{request.headers} +--------------------------------------------------------------------------------------- +""" + response_code = 404 + + return Response(response_code, {"content-type": "text/plain"}, bytes(response_content, "utf-8")) + print(f"Got request URI: {request.uri}") print(f"Here's my environment: {environ}") diff --git a/test/test.sh b/test/test.sh index 20eed31..db7eda7 100755 --- a/test/test.sh +++ b/test/test.sh @@ -12,7 +12,7 @@ echo "built the test app successfully" # Start the spin app in the background echo "Starting Spin app" -spin up --follow-all & +spin up & # wait for app to be up and running echo "Waiting for Spin app to be ready" @@ -21,6 +21,7 @@ timeout 60s bash -c 'until curl --silent -f http://localhost:3000/health > /dev/ # start the test echo -e "Starting test\n" curl -f http://localhost:3000 || isFailed=true +curl -f -H "spin-header-test-key1: value1" -H "spin-header-test-key2: value2" -H "spin-header-test-key1: value3" -H "spin-header-test-key2: value4" http://127.0.0.1:3000/duplicateheadertest || isFailed=true echo -e "\n\nTest completed" # kill the spin app