dabla commented on code in PR #72165:
URL: https://github.com/apache/airflow/pull/72165#discussion_r4307198291


##########
providers/http/src/airflow/providers/http/hooks/http.py:
##########
@@ -169,6 +169,35 @@ def _retryable_error_async(exception: BaseException) -> 
bool:
     return exception.status >= 500
 
 
+class _ConnectionHeaderSession(Session):

Review Comment:
   **[warning]** The behaviour change for `HttpHook` is not documented in this 
PR.
   
   The changelog and connection docs were asked for on #70000 (@aaron-y-chen in 
their review, and @Samin061 when closing it: "point the connection at the final 
host, or set `allow_redirects=False` in `extra_options` and make the second 
request yourself"). #72398 now adds a `.. warning::` to `changelog.rst` and a 
paragraph to `connections/http.rst`, but it describes the async hook ("follow 
aiohttp's origin rules"), and nothing in this PR covers the sync side. If the 
two PRs merge in separate provider waves, or this one first, `HttpHook` users 
get the change with no note.
   
   The note should also say that **every** connection header is dropped, not 
only credentials: `Accept` is dropped in 
`test_connection_headers_dropped_on_cross_host_redirect`, and so would be a 
`Content-Type` from the extra on a 307/308 that resends the body to the other 
host. Someone who relied on a CDN hop getting the same `Accept` will see that 
first.
   
   Suggestion: one hook-neutral note (both hooks, the origin rule from 
`should_strip_auth`, the two ways out quoted above), carried by whichever of 
#72165 / #72398 merges first, or by both if they go together.



##########
providers/http/tests/unit/http/hooks/test_http.py:
##########
@@ -1385,3 +1386,87 @@ async def 
test_resolve_srv_targets_async_missing_dependency_raises(self):
         with mock.patch.dict(sys.modules, {"dns": None, "dns.asyncresolver": 
None, "dns.exception": None}):
             with pytest.raises(HttpSrvLookupException, match="dnspython"):
                 await hook._resolve_srv_targets_async("_http._tcp.example.com")
+
+
+class TestConnectionHeadersAcrossRedirects:
+    """Connection-supplied headers must not follow a redirect to a different 
host.
+
+    ``requests`` strips only ``Authorization`` (``Session.rebuild_auth``), so 
a credential
+    carried in a differently-named header — the documented ``extra``-field 
pattern — would
+    otherwise be replayed to whatever host the redirect points at.
+    """
+
+    @staticmethod
+    def _redirect(session, old_url, new_url):

Review Comment:
   **[nit]** All the redirect tests call `rebuild_auth` directly, so no test 
goes through a real redirect.
   
   `_redirect` builds a prepared request by hand and calls 
`session.rebuild_auth`. The tests would still pass if `run()` / 
`run_and_check()` stopped going through `Session.send` → `resolve_redirects`, 
or if `requests` changed what it copies to the next hop. #72398 tests the async 
side end to end with an aiohttp `TestServer`. One test like it for the sync 
hook would cover the whole path. With `requests_mock`, the real redirect logic 
of the session still runs:
   
   ```python
   def test_run_drops_connection_header_on_cross_host_redirect(self, 
requests_mock):
       requests_mock.get("http://test:8080/v1/test";, status_code=302, 
headers={"Location": "https://cdn.example.com/f"})
       requests_mock.get("https://cdn.example.com/f";, text="ok")
       with mock.patch(
           "airflow.providers.http.hooks.http.HttpHook.get_connection",
           side_effect=get_airflow_connection_with_extra({"X-API-Key": 
"secret"}),
       ):
           HttpHook(method="GET").run("v1/test")
   
       assert requests_mock.request_history[0].headers["X-API-Key"] == "secret"
       assert "X-API-Key" not in requests_mock.request_history[-1].headers
   ```
   
   (I ran the equivalent locally against this head: the cross-host hop drops 
`X-API-Key` and `Accept`, a same-host hop keeps them, a caller header survives, 
and a port change strips. So this is about keeping it covered, not a bug.)



##########
providers/http/src/airflow/providers/http/hooks/http.py:
##########
@@ -265,10 +294,11 @@ def get_conn(
             session = self._configure_session_from_extra(session, connection, 
extra_options)
 
         session = self._configure_session_from_mount_adapters(session)
-        if self.default_headers:
-            session.headers.update(self.default_headers)
-        if headers:
-            session.headers.update(headers)
+        for override in (self.default_headers, headers):
+            if override:
+                session.headers.update(override)
+                if isinstance(session, _ConnectionHeaderSession):

Review Comment:
   **[nit]** The hook keeps the session's headers and `connection_header_keys` 
in sync from outside, behind two `isinstance` checks.
   
   `get_conn` (here) and `_configure_session_from_extra` (line 394) each ask 
the session for its type and then update two structures that must stay 
consistent: `session.headers` and `session.connection_header_keys`. If the 
session took both updates as one call, e.g. `add_connection_headers(headers)` 
and `add_caller_headers(headers)` on `_ConnectionHeaderSession`, the 
lower-casing and the add/remove rule would live in one place next to 
`rebuild_auth`, which reads them. The hook would then call a method instead of 
checking the type ([Martin, 
*OCP*](http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod)).
   
   The trade-off: the `isinstance` guard also covers a plain `Session` (the 
sensor tests' `FakeSession`, or a subclass passing its own session to 
`_configure_session_from_extra`). With methods, `FakeSession` would need to 
subclass `_ConnectionHeaderSession`. Fine to leave as is if you prefer that.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to