{"thread":{"id":"66196","subject":"[PATCH v2] http: preserve wwwauth_headers across redirects","startedAt":"2026-08-20T03:21:54Z","lastAt":"2026-08-25T00:40:52Z","messageCount":3,"participants":["Aaron Plattner","Junio C Hamano"],"isPatch":true,"patchVersion":2,"patchTotal":null},"messages":[{"id":"550859","messageId":"20260819-http-preserve-wwwauth-redirect-v2-1-4c61039432b0@nvidia.com","threadId":"66196","inReplyTo":null,"subject":"[PATCH v2] http: preserve wwwauth_headers across redirects","fromName":"Aaron Plattner","fromEmail":"aplattner@nvidia.com","sentAt":"2026-08-20T03:21:04Z","receivedAt":"2026-08-20T03:21:54Z","isPatch":true,"body":"When cURL follows a redirect, it calls the CURLOPT_HEADERFUNCTION for\neach header received including ones from a redirect. http_request() sets\nfwrite_wwwauth() as the header function, which will record the wwwauth[]\nentries for the last step in the redirection chain.\n\nHowever, when http_request_recoverable() sees that cURL followed a\nredirect, it attempts to update the credentials for the request from the\nnew URL using credential_from_url(). The first thing that does is call\ncredential_clear(), which clears everything including wwwauth_headers.\n\nIf the new URL should use a credential helper rather than credentials\nembedded in the URL, this loses the list of authentication methods that\nthe server provided in the redirect.\n\nThe WWW-Authenticate challenge is not derived from the URL; it is\npopulated from the server's response, and after a redirect it describes\nhow to authenticate to the redirect target and it needs to survive the\nURL update so that credential helpers can know which authentication\nmethods are allowed.\n\nAdd a new credential_update_url() that wraps credential_from_url() and\npreserves wwwauth_headers specifically. Use SWAP() to avoid having to\ncopy the whole strbuf.\n\nSigned-off-by: Aaron Plattner <aplattner@nvidia.com>\n---\nI decided to come back to this after I noticed that at least one other\nperson had run into the same bug:\n\nhttps://lore.kernel.org/all/CADoNwcscDrx+YcfbcW4YKONDZZQgnPiwEOxL4QYV_C7_=FOFcg@mail.gmail.com/\n\nRather than reworking everything about how credentials are stored, I\ntook your advice in [1] and just moved the code to preserve the wwwauth_headers\ninto credential.c. That way any future credential fields that need to be\npreserved can be added there without having to hunt down other places\nlike http.c that are reaching into it.\n\n[1] https://lore.kernel.org/all/xmqqpl28scll.fsf@gitster.g/\n---\nChanges in v2:\n- Move strvec preservation into a helper function in credential.c\n- Use SWAP instead of strvec_pushv() to avoid having to copy the\n  contents of the strvec.\n- Link to v1: https://patch.msgid.link/20260602161150.1527493-1-aplattner@nvidia.com\n---\n credential.c                | 16 ++++++++++++++++\n credential.h                |  8 ++++++++\n http.c                      |  9 ++++++++-\n t/lib-httpd/apache.conf     |  1 +\n t/t5563-simple-http-auth.sh | 45 +++++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 78 insertions(+), 1 deletion(-)\n\ndiff --git a/credential.c b/credential.c\nindex 2594c0c422..035399d7ee 100644\n--- a/credential.c\n+++ b/credential.c\n@@ -708,3 +708,19 @@ void credential_from_url(struct credential *c, const char *url)\n \tif (credential_from_url_gently(c, url, 0) < 0)\n \t\tdie(_(\"credential url cannot be parsed: %s\"), url);\n }\n+\n+void credential_update_url(struct credential *c, const char *url)\n+{\n+\tstruct strvec wwwauth_headers = STRVEC_INIT;\n+\n+\t/*\n+\t * credential_from_url() clears the whole credential. Preserve the\n+\t * WWW-Authenticate list, which is derived from the server's original\n+\t * response rather than from the URL and is required to authenticate to\n+\t * the new URL.\n+\t */\n+\tSWAP(wwwauth_headers, c->wwwauth_headers);\n+\tcredential_from_url(c, url);\n+\tSWAP(c->wwwauth_headers, wwwauth_headers);\n+\tstrvec_clear(&wwwauth_headers);\n+}\ndiff --git a/credential.h b/credential.h\nindex c78b72d110..b90f666e33 100644\n--- a/credential.h\n+++ b/credential.h\n@@ -305,6 +305,14 @@ void credential_write(const struct credential *, FILE *,\n void credential_from_url(struct credential *, const char *url);\n int credential_from_url_gently(struct credential *, const char *url, int quiet);\n \n+/*\n+ * Update the URL-derived fields (protocol, host, path) of an existing\n+ * credential to match a new URL. Unlike credential_from_url(), this function\n+ * preserves state that was derived from a server's HTTP redirect response,\n+ * such as the WWW-Authenticate headers.\n+ */\n+void credential_update_url(struct credential *c, const char *url);\n+\n int credential_match(const struct credential *want,\n \t\t     const struct credential *have, int match_password);\n \ndiff --git a/http.c b/http.c\nindex a0d399b274..e8abb9f95a 100644\n--- a/http.c\n+++ b/http.c\n@@ -2427,7 +2427,14 @@ static int http_request_recoverable(const char *url,\n \tif (options->effective_url && options->base_url) {\n \t\tif (update_url_from_redirect(options->base_url,\n \t\t\t\t\t     url, options->effective_url)) {\n-\t\t\tcredential_from_url(&http_auth, options->base_url->buf);\n+\t\t\t/*\n+\t\t\t * Use credential_update_url() rather than\n+\t\t\t * credential_from_url() so that the WWW-Authenticate\n+\t\t\t * challenge the server sent with the redirect target's\n+\t\t\t * response is preserved and handed to the credential\n+\t\t\t * helper.\n+\t\t\t */\n+\t\t\tcredential_update_url(&http_auth, options->base_url->buf);\n \t\t\turl = options->effective_url->buf;\n \t\t}\n \t}\ndiff --git a/t/lib-httpd/apache.conf b/t/lib-httpd/apache.conf\nindex 4149fc1078..0627ef1433 100644\n--- a/t/lib-httpd/apache.conf\n+++ b/t/lib-httpd/apache.conf\n@@ -203,6 +203,7 @@ RewriteRule ^/dumb-redir/(.*)$ /dumb/$1 [R=301]\n RewriteRule ^/smart-redir-perm/(.*)$ /smart/$1 [R=301]\n RewriteRule ^/smart-redir-temp/(.*)$ /smart/$1 [R=302]\n RewriteRule ^/smart-redir-auth/(.*)$ /auth/smart/$1 [R=301]\n+RewriteRule ^/custom_auth_redir/(.*)$ /custom_auth/$1 [R=302]\n RewriteRule ^/smart-redir-limited/(.*)/info/refs$ /smart/$1/info/refs [R=301]\n RewriteRule ^/ftp-redir/(.*)$ ftp://localhost:1000/$1 [R=302]\n \ndiff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh\nindex a7d475dd68..349ae4ab39 100755\n--- a/t/t5563-simple-http-auth.sh\n+++ b/t/t5563-simple-http-auth.sh\n@@ -557,6 +557,51 @@ test_expect_success 'access using bearer auth' '\n \tEOF\n '\n \n+test_expect_success 'bearer auth after redirect preserves wwwauth headers' '\n+\ttest_when_finished \"per_test_cleanup\" &&\n+\n+\tset_credential_reply get <<-EOF &&\n+\tcapability[]=authtype\n+\tauthtype=Bearer\n+\tcredential=YS1naXQtdG9rZW4=\n+\tEOF\n+\n+\tcat >\"$HTTPD_ROOT_PATH/custom-auth.valid\" <<-EOF &&\n+\tid=1 creds=Bearer YS1naXQtdG9rZW4=\n+\tEOF\n+\n+\tcat >\"$HTTPD_ROOT_PATH/custom-auth.challenge\" <<-EOF &&\n+\tid=1 status=200\n+\tid=default response=WWW-Authenticate: FooBar param1=\"value1\" param2=\"value2\"\n+\tid=default response=WWW-Authenticate: Bearer authorize_uri=\"id.example.com\" p=1 q=0\n+\tid=default response=WWW-Authenticate: Basic realm=\"example.com\"\n+\tEOF\n+\n+\ttest_config_global credential.helper test-helper &&\n+\ttest_config_global credential.useHttpPath true &&\n+\tgit ls-remote \"$HTTPD_URL/custom_auth_redir/repo.git\" &&\n+\n+\texpect_credential_query get <<-EOF &&\n+\tcapability[]=authtype\n+\tcapability[]=state\n+\tprotocol=http\n+\thost=$HTTPD_DEST\n+\tpath=custom_auth/repo.git\n+\twwwauth[]=FooBar param1=\"value1\" param2=\"value2\"\n+\twwwauth[]=Bearer authorize_uri=\"id.example.com\" p=1 q=0\n+\twwwauth[]=Basic realm=\"example.com\"\n+\tEOF\n+\n+\texpect_credential_query store <<-EOF\n+\tcapability[]=authtype\n+\tauthtype=Bearer\n+\tcredential=YS1naXQtdG9rZW4=\n+\tprotocol=http\n+\thost=$HTTPD_DEST\n+\tpath=custom_auth/repo.git\n+\tEOF\n+'\n+\n test_expect_success 'access using bearer auth with invalid credentials' '\n \ttest_when_finished \"per_test_cleanup\" &&\n \n\n---\nbase-commit: dea0ea3582e6980ddbc1173cc8e3e9f9db91cde0\nchange-id: 20260819-http-preserve-wwwauth-redirect-a3fe4dab6b35\n\n"},{"id":"550912","messageId":"xmqq8q60u82x.fsf@gitster.g","threadId":"66196","inReplyTo":"20260819-http-preserve-wwwauth-redirect-v2-1-4c61039432b0@nvidia.com","subject":"Re: [PATCH v2] http: preserve wwwauth_headers across redirects","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-20T15:26:14Z","receivedAt":"2026-08-20T15:26:17Z","isPatch":true,"body":"For those of you who are watching from the sidelines, this v2 lacks\nthe threading history.\n\nThe v1 is at https://lore.kernel.org/git/20260602161150.1527493-1-aplattner@nvidia.com/\n\nThanks.\n\nAaron Plattner <aplattner@nvidia.com> writes:\n\n> When cURL follows a redirect, it calls the CURLOPT_HEADERFUNCTION for\n> each header received including ones from a redirect. http_request() sets\n> fwrite_wwwauth() as the header function, which will record the wwwauth[]\n> entries for the last step in the redirection chain.\n>\n> However, when http_request_recoverable() sees that cURL followed a\n> redirect, it attempts to update the credentials for the request from the\n> new URL using credential_from_url(). The first thing that does is call\n> credential_clear(), which clears everything including wwwauth_headers.\n>\n> If the new URL should use a credential helper rather than credentials\n> embedded in the URL, this loses the list of authentication methods that\n> the server provided in the redirect.\n>\n> The WWW-Authenticate challenge is not derived from the URL; it is\n> populated from the server's response, and after a redirect it describes\n> how to authenticate to the redirect target and it needs to survive the\n> URL update so that credential helpers can know which authentication\n> methods are allowed.\n>\n> Add a new credential_update_url() that wraps credential_from_url() and\n> preserves wwwauth_headers specifically. Use SWAP() to avoid having to\n> copy the whole strbuf.\n>\n> Signed-off-by: Aaron Plattner <aplattner@nvidia.com>\n> ---\n> I decided to come back to this after I noticed that at least one other\n> person had run into the same bug:\n>\n> https://lore.kernel.org/all/CADoNwcscDrx+YcfbcW4YKONDZZQgnPiwEOxL4QYV_C7_=FOFcg@mail.gmail.com/\n>\n> Rather than reworking everything about how credentials are stored, I\n> took your advice in [1] and just moved the code to preserve the wwwauth_headers\n> into credential.c. That way any future credential fields that need to be\n> preserved can be added there without having to hunt down other places\n> like http.c that are reaching into it.\n>\n> [1] https://lore.kernel.org/all/xmqqpl28scll.fsf@gitster.g/\n> ---\n> Changes in v2:\n> - Move strvec preservation into a helper function in credential.c\n> - Use SWAP instead of strvec_pushv() to avoid having to copy the\n>   contents of the strvec.\n> - Link to v1: https://patch.msgid.link/20260602161150.1527493-1-aplattner@nvidia.com\n> ---\n>  credential.c                | 16 ++++++++++++++++\n>  credential.h                |  8 ++++++++\n>  http.c                      |  9 ++++++++-\n>  t/lib-httpd/apache.conf     |  1 +\n>  t/t5563-simple-http-auth.sh | 45 +++++++++++++++++++++++++++++++++++++++++++++\n>  5 files changed, 78 insertions(+), 1 deletion(-)\n>\n> diff --git a/credential.c b/credential.c\n> index 2594c0c422..035399d7ee 100644\n> --- a/credential.c\n> +++ b/credential.c\n> @@ -708,3 +708,19 @@ void credential_from_url(struct credential *c, const char *url)\n>  \tif (credential_from_url_gently(c, url, 0) < 0)\n>  \t\tdie(_(\"credential url cannot be parsed: %s\"), url);\n>  }\n> +\n> +void credential_update_url(struct credential *c, const char *url)\n> +{\n> +\tstruct strvec wwwauth_headers = STRVEC_INIT;\n> +\n> +\t/*\n> +\t * credential_from_url() clears the whole credential. Preserve the\n> +\t * WWW-Authenticate list, which is derived from the server's original\n> +\t * response rather than from the URL and is required to authenticate to\n> +\t * the new URL.\n> +\t */\n> +\tSWAP(wwwauth_headers, c->wwwauth_headers);\n> +\tcredential_from_url(c, url);\n> +\tSWAP(c->wwwauth_headers, wwwauth_headers);\n> +\tstrvec_clear(&wwwauth_headers);\n> +}\n> diff --git a/credential.h b/credential.h\n> index c78b72d110..b90f666e33 100644\n> --- a/credential.h\n> +++ b/credential.h\n> @@ -305,6 +305,14 @@ void credential_write(const struct credential *, FILE *,\n>  void credential_from_url(struct credential *, const char *url);\n>  int credential_from_url_gently(struct credential *, const char *url, int quiet);\n>  \n> +/*\n> + * Update the URL-derived fields (protocol, host, path) of an existing\n> + * credential to match a new URL. Unlike credential_from_url(), this function\n> + * preserves state that was derived from a server's HTTP redirect response,\n> + * such as the WWW-Authenticate headers.\n> + */\n> +void credential_update_url(struct credential *c, const char *url);\n> +\n>  int credential_match(const struct credential *want,\n>  \t\t     const struct credential *have, int match_password);\n>  \n> diff --git a/http.c b/http.c\n> index a0d399b274..e8abb9f95a 100644\n> --- a/http.c\n> +++ b/http.c\n> @@ -2427,7 +2427,14 @@ static int http_request_recoverable(const char *url,\n>  \tif (options->effective_url && options->base_url) {\n>  \t\tif (update_url_from_redirect(options->base_url,\n>  \t\t\t\t\t     url, options->effective_url)) {\n> -\t\t\tcredential_from_url(&http_auth, options->base_url->buf);\n> +\t\t\t/*\n> +\t\t\t * Use credential_update_url() rather than\n> +\t\t\t * credential_from_url() so that the WWW-Authenticate\n> +\t\t\t * challenge the server sent with the redirect target's\n> +\t\t\t * response is preserved and handed to the credential\n> +\t\t\t * helper.\n> +\t\t\t */\n> +\t\t\tcredential_update_url(&http_auth, options->base_url->buf);\n>  \t\t\turl = options->effective_url->buf;\n>  \t\t}\n>  \t}\n> diff --git a/t/lib-httpd/apache.conf b/t/lib-httpd/apache.conf\n> index 4149fc1078..0627ef1433 100644\n> --- a/t/lib-httpd/apache.conf\n> +++ b/t/lib-httpd/apache.conf\n> @@ -203,6 +203,7 @@ RewriteRule ^/dumb-redir/(.*)$ /dumb/$1 [R=301]\n>  RewriteRule ^/smart-redir-perm/(.*)$ /smart/$1 [R=301]\n>  RewriteRule ^/smart-redir-temp/(.*)$ /smart/$1 [R=302]\n>  RewriteRule ^/smart-redir-auth/(.*)$ /auth/smart/$1 [R=301]\n> +RewriteRule ^/custom_auth_redir/(.*)$ /custom_auth/$1 [R=302]\n>  RewriteRule ^/smart-redir-limited/(.*)/info/refs$ /smart/$1/info/refs [R=301]\n>  RewriteRule ^/ftp-redir/(.*)$ ftp://localhost:1000/$1 [R=302]\n>  \n> diff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh\n> index a7d475dd68..349ae4ab39 100755\n> --- a/t/t5563-simple-http-auth.sh\n> +++ b/t/t5563-simple-http-auth.sh\n> @@ -557,6 +557,51 @@ test_expect_success 'access using bearer auth' '\n>  \tEOF\n>  '\n>  \n> +test_expect_success 'bearer auth after redirect preserves wwwauth headers' '\n> +\ttest_when_finished \"per_test_cleanup\" &&\n> +\n> +\tset_credential_reply get <<-EOF &&\n> +\tcapability[]=authtype\n> +\tauthtype=Bearer\n> +\tcredential=YS1naXQtdG9rZW4=\n> +\tEOF\n> +\n> +\tcat >\"$HTTPD_ROOT_PATH/custom-auth.valid\" <<-EOF &&\n> +\tid=1 creds=Bearer YS1naXQtdG9rZW4=\n> +\tEOF\n> +\n> +\tcat >\"$HTTPD_ROOT_PATH/custom-auth.challenge\" <<-EOF &&\n> +\tid=1 status=200\n> +\tid=default response=WWW-Authenticate: FooBar param1=\"value1\" param2=\"value2\"\n> +\tid=default response=WWW-Authenticate: Bearer authorize_uri=\"id.example.com\" p=1 q=0\n> +\tid=default response=WWW-Authenticate: Basic realm=\"example.com\"\n> +\tEOF\n> +\n> +\ttest_config_global credential.helper test-helper &&\n> +\ttest_config_global credential.useHttpPath true &&\n> +\tgit ls-remote \"$HTTPD_URL/custom_auth_redir/repo.git\" &&\n> +\n> +\texpect_credential_query get <<-EOF &&\n> +\tcapability[]=authtype\n> +\tcapability[]=state\n> +\tprotocol=http\n> +\thost=$HTTPD_DEST\n> +\tpath=custom_auth/repo.git\n> +\twwwauth[]=FooBar param1=\"value1\" param2=\"value2\"\n> +\twwwauth[]=Bearer authorize_uri=\"id.example.com\" p=1 q=0\n> +\twwwauth[]=Basic realm=\"example.com\"\n> +\tEOF\n> +\n> +\texpect_credential_query store <<-EOF\n> +\tcapability[]=authtype\n> +\tauthtype=Bearer\n> +\tcredential=YS1naXQtdG9rZW4=\n> +\tprotocol=http\n> +\thost=$HTTPD_DEST\n> +\tpath=custom_auth/repo.git\n> +\tEOF\n> +'\n> +\n>  test_expect_success 'access using bearer auth with invalid credentials' '\n>  \ttest_when_finished \"per_test_cleanup\" &&\n>  \n>\n> ---\n> base-commit: dea0ea3582e6980ddbc1173cc8e3e9f9db91cde0\n> change-id: 20260819-http-preserve-wwwauth-redirect-a3fe4dab6b35\n"},{"id":"551164","messageId":"xmqqbjarowvi.fsf@gitster.g","threadId":"66196","inReplyTo":"xmqq8q60u82x.fsf@gitster.g","subject":"Re: [PATCH v2] http: preserve wwwauth_headers across redirects","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-08-25T00:40:49Z","receivedAt":"2026-08-25T00:40:52Z","isPatch":true,"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> For those of you who are watching from the sidelines, this v2 lacks\n> the threading history.\n>\n> The v1 is at https://lore.kernel.org/git/20260602161150.1527493-1-aplattner@nvidia.com/\n\nAny takers?  It should be quite easy to be more interested and be a\nbetter reviewer in the http transfer codepaths than I am.\n\nThanks.\n\n> Aaron Plattner <aplattner@nvidia.com> writes:\n>\n>> When cURL follows a redirect, it calls the CURLOPT_HEADERFUNCTION for\n>> each header received including ones from a redirect. http_request() sets\n>> fwrite_wwwauth() as the header function, which will record the wwwauth[]\n>> entries for the last step in the redirection chain.\n>>\n>> However, when http_request_recoverable() sees that cURL followed a\n>> redirect, it attempts to update the credentials for the request from the\n>> new URL using credential_from_url(). The first thing that does is call\n>> credential_clear(), which clears everything including wwwauth_headers.\n>>\n>> If the new URL should use a credential helper rather than credentials\n>> embedded in the URL, this loses the list of authentication methods that\n>> the server provided in the redirect.\n>>\n>> The WWW-Authenticate challenge is not derived from the URL; it is\n>> populated from the server's response, and after a redirect it describes\n>> how to authenticate to the redirect target and it needs to survive the\n>> URL update so that credential helpers can know which authentication\n>> methods are allowed.\n>>\n>> Add a new credential_update_url() that wraps credential_from_url() and\n>> preserves wwwauth_headers specifically. Use SWAP() to avoid having to\n>> copy the whole strbuf.\n>>\n>> Signed-off-by: Aaron Plattner <aplattner@nvidia.com>\n>> ---\n>> I decided to come back to this after I noticed that at least one other\n>> person had run into the same bug:\n>>\n>> https://lore.kernel.org/all/CADoNwcscDrx+YcfbcW4YKONDZZQgnPiwEOxL4QYV_C7_=FOFcg@mail.gmail.com/\n>>\n>> Rather than reworking everything about how credentials are stored, I\n>> took your advice in [1] and just moved the code to preserve the wwwauth_headers\n>> into credential.c. That way any future credential fields that need to be\n>> preserved can be added there without having to hunt down other places\n>> like http.c that are reaching into it.\n>>\n>> [1] https://lore.kernel.org/all/xmqqpl28scll.fsf@gitster.g/\n>> ---\n>> Changes in v2:\n>> - Move strvec preservation into a helper function in credential.c\n>> - Use SWAP instead of strvec_pushv() to avoid having to copy the\n>>   contents of the strvec.\n>> - Link to v1: https://patch.msgid.link/20260602161150.1527493-1-aplattner@nvidia.com\n>> ---\n>>  credential.c                | 16 ++++++++++++++++\n>>  credential.h                |  8 ++++++++\n>>  http.c                      |  9 ++++++++-\n>>  t/lib-httpd/apache.conf     |  1 +\n>>  t/t5563-simple-http-auth.sh | 45 +++++++++++++++++++++++++++++++++++++++++++++\n>>  5 files changed, 78 insertions(+), 1 deletion(-)\n>>\n>> diff --git a/credential.c b/credential.c\n>> index 2594c0c422..035399d7ee 100644\n>> --- a/credential.c\n>> +++ b/credential.c\n>> @@ -708,3 +708,19 @@ void credential_from_url(struct credential *c, const char *url)\n>>  \tif (credential_from_url_gently(c, url, 0) < 0)\n>>  \t\tdie(_(\"credential url cannot be parsed: %s\"), url);\n>>  }\n>> +\n>> +void credential_update_url(struct credential *c, const char *url)\n>> +{\n>> +\tstruct strvec wwwauth_headers = STRVEC_INIT;\n>> +\n>> +\t/*\n>> +\t * credential_from_url() clears the whole credential. Preserve the\n>> +\t * WWW-Authenticate list, which is derived from the server's original\n>> +\t * response rather than from the URL and is required to authenticate to\n>> +\t * the new URL.\n>> +\t */\n>> +\tSWAP(wwwauth_headers, c->wwwauth_headers);\n>> +\tcredential_from_url(c, url);\n>> +\tSWAP(c->wwwauth_headers, wwwauth_headers);\n>> +\tstrvec_clear(&wwwauth_headers);\n>> +}\n>> diff --git a/credential.h b/credential.h\n>> index c78b72d110..b90f666e33 100644\n>> --- a/credential.h\n>> +++ b/credential.h\n>> @@ -305,6 +305,14 @@ void credential_write(const struct credential *, FILE *,\n>>  void credential_from_url(struct credential *, const char *url);\n>>  int credential_from_url_gently(struct credential *, const char *url, int quiet);\n>>  \n>> +/*\n>> + * Update the URL-derived fields (protocol, host, path) of an existing\n>> + * credential to match a new URL. Unlike credential_from_url(), this function\n>> + * preserves state that was derived from a server's HTTP redirect response,\n>> + * such as the WWW-Authenticate headers.\n>> + */\n>> +void credential_update_url(struct credential *c, const char *url);\n>> +\n>>  int credential_match(const struct credential *want,\n>>  \t\t     const struct credential *have, int match_password);\n>>  \n>> diff --git a/http.c b/http.c\n>> index a0d399b274..e8abb9f95a 100644\n>> --- a/http.c\n>> +++ b/http.c\n>> @@ -2427,7 +2427,14 @@ static int http_request_recoverable(const char *url,\n>>  \tif (options->effective_url && options->base_url) {\n>>  \t\tif (update_url_from_redirect(options->base_url,\n>>  \t\t\t\t\t     url, options->effective_url)) {\n>> -\t\t\tcredential_from_url(&http_auth, options->base_url->buf);\n>> +\t\t\t/*\n>> +\t\t\t * Use credential_update_url() rather than\n>> +\t\t\t * credential_from_url() so that the WWW-Authenticate\n>> +\t\t\t * challenge the server sent with the redirect target's\n>> +\t\t\t * response is preserved and handed to the credential\n>> +\t\t\t * helper.\n>> +\t\t\t */\n>> +\t\t\tcredential_update_url(&http_auth, options->base_url->buf);\n>>  \t\t\turl = options->effective_url->buf;\n>>  \t\t}\n>>  \t}\n>> diff --git a/t/lib-httpd/apache.conf b/t/lib-httpd/apache.conf\n>> index 4149fc1078..0627ef1433 100644\n>> --- a/t/lib-httpd/apache.conf\n>> +++ b/t/lib-httpd/apache.conf\n>> @@ -203,6 +203,7 @@ RewriteRule ^/dumb-redir/(.*)$ /dumb/$1 [R=301]\n>>  RewriteRule ^/smart-redir-perm/(.*)$ /smart/$1 [R=301]\n>>  RewriteRule ^/smart-redir-temp/(.*)$ /smart/$1 [R=302]\n>>  RewriteRule ^/smart-redir-auth/(.*)$ /auth/smart/$1 [R=301]\n>> +RewriteRule ^/custom_auth_redir/(.*)$ /custom_auth/$1 [R=302]\n>>  RewriteRule ^/smart-redir-limited/(.*)/info/refs$ /smart/$1/info/refs [R=301]\n>>  RewriteRule ^/ftp-redir/(.*)$ ftp://localhost:1000/$1 [R=302]\n>>  \n>> diff --git a/t/t5563-simple-http-auth.sh b/t/t5563-simple-http-auth.sh\n>> index a7d475dd68..349ae4ab39 100755\n>> --- a/t/t5563-simple-http-auth.sh\n>> +++ b/t/t5563-simple-http-auth.sh\n>> @@ -557,6 +557,51 @@ test_expect_success 'access using bearer auth' '\n>>  \tEOF\n>>  '\n>>  \n>> +test_expect_success 'bearer auth after redirect preserves wwwauth headers' '\n>> +\ttest_when_finished \"per_test_cleanup\" &&\n>> +\n>> +\tset_credential_reply get <<-EOF &&\n>> +\tcapability[]=authtype\n>> +\tauthtype=Bearer\n>> +\tcredential=YS1naXQtdG9rZW4=\n>> +\tEOF\n>> +\n>> +\tcat >\"$HTTPD_ROOT_PATH/custom-auth.valid\" <<-EOF &&\n>> +\tid=1 creds=Bearer YS1naXQtdG9rZW4=\n>> +\tEOF\n>> +\n>> +\tcat >\"$HTTPD_ROOT_PATH/custom-auth.challenge\" <<-EOF &&\n>> +\tid=1 status=200\n>> +\tid=default response=WWW-Authenticate: FooBar param1=\"value1\" param2=\"value2\"\n>> +\tid=default response=WWW-Authenticate: Bearer authorize_uri=\"id.example.com\" p=1 q=0\n>> +\tid=default response=WWW-Authenticate: Basic realm=\"example.com\"\n>> +\tEOF\n>> +\n>> +\ttest_config_global credential.helper test-helper &&\n>> +\ttest_config_global credential.useHttpPath true &&\n>> +\tgit ls-remote \"$HTTPD_URL/custom_auth_redir/repo.git\" &&\n>> +\n>> +\texpect_credential_query get <<-EOF &&\n>> +\tcapability[]=authtype\n>> +\tcapability[]=state\n>> +\tprotocol=http\n>> +\thost=$HTTPD_DEST\n>> +\tpath=custom_auth/repo.git\n>> +\twwwauth[]=FooBar param1=\"value1\" param2=\"value2\"\n>> +\twwwauth[]=Bearer authorize_uri=\"id.example.com\" p=1 q=0\n>> +\twwwauth[]=Basic realm=\"example.com\"\n>> +\tEOF\n>> +\n>> +\texpect_credential_query store <<-EOF\n>> +\tcapability[]=authtype\n>> +\tauthtype=Bearer\n>> +\tcredential=YS1naXQtdG9rZW4=\n>> +\tprotocol=http\n>> +\thost=$HTTPD_DEST\n>> +\tpath=custom_auth/repo.git\n>> +\tEOF\n>> +'\n>> +\n>>  test_expect_success 'access using bearer auth with invalid credentials' '\n>>  \ttest_when_finished \"per_test_cleanup\" &&\n>>  \n>>\n>> ---\n>> base-commit: dea0ea3582e6980ddbc1173cc8e3e9f9db91cde0\n>> change-id: 20260819-http-preserve-wwwauth-redirect-a3fe4dab6b35\n"}]}