1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
|
From: Illia Volochii <illia.volochii@gmail.com>
Date: Wed, 18 Jun 2025 16:30:35 +0300
Subject: Merge commit from fork
Origin: upstream, https://github.com/urllib3/urllib3/commit/7eb4a2aafe49a279c29b6d1f0ed0f42e9736194f
Bug-Debian: https://bugs.debian.org/1108077
Last-Update: 2025-07-13
---
docs/reference/contrib/emscripten.rst | 2 +-
src/urllib3/contrib/emscripten/fetch.py | 20 +++++++++++++
test/contrib/emscripten/test_emscripten.py | 46 ++++++++++++++++++++++++++++++
3 files changed, 67 insertions(+), 1 deletion(-)
diff --git a/docs/reference/contrib/emscripten.rst b/docs/reference/contrib/emscripten.rst
index a8f1cda..4670757 100644
--- a/docs/reference/contrib/emscripten.rst
+++ b/docs/reference/contrib/emscripten.rst
@@ -65,7 +65,7 @@ Features which are usable with Emscripten support are:
* Timeouts
* Retries
* Streaming (with Web Workers and Cross-Origin Isolation)
-* Redirects (determined by browser/runtime, not restrictable with urllib3)
+* Redirects (urllib3 controls redirects in Node.js but not in browsers where behavior is determined by runtime)
* Decompressing response bodies
Features which don't work with Emscripten:
diff --git a/src/urllib3/contrib/emscripten/fetch.py b/src/urllib3/contrib/emscripten/fetch.py
index a514306..6695821 100644
--- a/src/urllib3/contrib/emscripten/fetch.py
+++ b/src/urllib3/contrib/emscripten/fetch.py
@@ -573,6 +573,11 @@ def send_jspi_request(
"method": request.method,
"signal": js_abort_controller.signal,
}
+ # Node.js returns the whole response (unlike opaqueredirect in browsers),
+ # so urllib3 can set `redirect: manual` to control redirects itself.
+ # https://stackoverflow.com/a/78524615
+ if _is_node_js():
+ fetch_data["redirect"] = "manual"
# Call JavaScript fetch (async api, returns a promise)
fetcher_promise_js = js.fetch(request.url, _obj_from_dict(fetch_data))
# Now suspend WebAssembly until we resolve that promise
@@ -693,6 +698,21 @@ def has_jspi() -> bool:
return False
+def _is_node_js() -> bool:
+ """
+ Check if we are in Node.js.
+
+ :return: True if we are in Node.js.
+ :rtype: bool
+ """
+ return (
+ hasattr(js, "process")
+ and hasattr(js.process, "release")
+ # According to the Node.js documentation, the release name is always "node".
+ and js.process.release.name == "node"
+ )
+
+
def streaming_ready() -> bool | None:
if _fetcher:
return _fetcher.streaming_ready
diff --git a/test/contrib/emscripten/test_emscripten.py b/test/contrib/emscripten/test_emscripten.py
index 5eaa674..fbf89fc 100644
--- a/test/contrib/emscripten/test_emscripten.py
+++ b/test/contrib/emscripten/test_emscripten.py
@@ -960,6 +960,52 @@ def test_redirects(
)
+@pytest.mark.with_jspi
+def test_disabled_redirects(
+ selenium_coverage: typing.Any, testserver_http: PyodideServerInfo
+) -> None:
+ """
+ Test that urllib3 can control redirects in Node.js.
+ """
+
+ @run_in_pyodide # type: ignore[misc]
+ def pyodide_test(selenium_coverage: typing.Any, host: str, port: int) -> None:
+ import pytest
+
+ from urllib3 import PoolManager, request
+ from urllib3.contrib.emscripten.fetch import _is_node_js
+ from urllib3.exceptions import MaxRetryError
+
+ if not _is_node_js():
+ pytest.skip("urllib3 does not control redirects in browsers.")
+
+ redirect_url = f"http://{host}:{port}/redirect"
+
+ with PoolManager(retries=0) as http:
+ with pytest.raises(MaxRetryError):
+ http.request("GET", redirect_url)
+
+ response = http.request("GET", redirect_url, redirect=False)
+ assert response.status == 303
+
+ with PoolManager(retries=False) as http:
+ response = http.request("GET", redirect_url)
+ assert response.status == 303
+
+ with pytest.raises(MaxRetryError):
+ request("GET", redirect_url, retries=0)
+
+ response = request("GET", redirect_url, redirect=False)
+ assert response.status == 303
+
+ response = request("GET", redirect_url, retries=0, redirect=False)
+ assert response.status == 303
+
+ pyodide_test(
+ selenium_coverage, testserver_http.http_host, testserver_http.http_port
+ )
+
+
def test_insecure_requests_warning(
selenium_coverage: typing.Any, testserver_http: PyodideServerInfo
) -> None:
|