http: on 303, switch to GET

... unless it is a POST and the user explicitly asked to keep doing
POST.

Add test1983/1984: verify --follow with 303 and PUT + custom GET

Fixes #20715
Reported-by: Dan Arnfield
Closes #21280
This commit is contained in:
Daniel Stenberg 2026-04-09 23:43:55 +02:00
parent bb3670f929
commit e5087ac9fc
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2
6 changed files with 169 additions and 24 deletions

View File

@ -94,8 +94,9 @@ change PUT etc - and therefore also not when libcurl issues a custom PUT. A
(except for HEAD).
To control for which of the 301/302/303 status codes libcurl should *not*
switch back to GET for when doing a custom POST, and instead keep the custom
method, use CURLOPT_POSTREDIR(3).
switch back to GET for when doing a custom POST (a POST transfer using a
modified method), and instead keep the custom method, use
CURLOPT_POSTREDIR(3).
If you prefer a custom POST method to be reset to exactly the method `POST`,
use CURLFOLLOW_FIRSTONLY instead.

View File

@ -32,19 +32,22 @@ CURLcode curl_easy_setopt(CURL *handle, CURLOPT_POSTREDIR,
Pass a bitmask to control how libcurl acts on redirects after POSTs that get a
301, 302 or 303 response back. A parameter with bit 0 set (value
**CURL_REDIR_POST_301**) tells the library to respect RFC 7231 (section
6.4.2 to 6.4.4) and not convert POST requests into GET requests when following
a 301 redirection. Setting bit 1 (value **CURL_REDIR_POST_302**) makes
libcurl maintain the request method after a 302 redirect whilst setting bit 2
(value **CURL_REDIR_POST_303**) makes libcurl maintain the request method
after a 303 redirect. The value **CURL_REDIR_POST_ALL** is a convenience
define that sets all three bits.
**CURL_REDIR_POST_301**) tells the library to not convert POST requests into
GET requests when following a 301 redirection. Setting bit 1 (value
**CURL_REDIR_POST_302**) makes libcurl maintain the request method after a 302
redirect whilst setting bit 2 (value **CURL_REDIR_POST_303**) makes libcurl
maintain the request method after a 303 redirect. The value
**CURL_REDIR_POST_ALL** is a convenience define that sets all three bits.
The non-RFC behavior is ubiquitous in web browsers, so the library does the
conversion by default to maintain consistency. A server may require a POST to
remain a POST after such a redirection. This option is meaningful only when
setting CURLOPT_FOLLOWLOCATION(3).
This option affects transfers where libcurl has been told to use HTTP POST
using for example CURLOPT_POST(3) or CURLOPT_MIMEPOST(3) and not if the
method has merely been modified with CURLOPT_CUSTOMREQUEST(3).
# DEFAULT
0

View File

@ -1115,6 +1115,11 @@ static void http_switch_to_get(struct Curl_easy *data, int code)
Curl_creader_set_rewind(data, FALSE);
}
#define HTTPREQ_IS_POST(data) \
((data)->state.httpreq == HTTPREQ_POST || \
(data)->state.httpreq == HTTPREQ_POST_FORM || \
(data)->state.httpreq == HTTPREQ_POST_MIME)
CURLcode Curl_http_follow(struct Curl_easy *data, const char *newurl,
followtype type)
{
@ -1323,10 +1328,7 @@ CURLcode Curl_http_follow(struct Curl_easy *data, const char *newurl,
* This behavior is forbidden by RFC1945 and the obsolete RFC2616, and
* can be overridden with CURLOPT_POSTREDIR.
*/
if((data->state.httpreq == HTTPREQ_POST ||
data->state.httpreq == HTTPREQ_POST_FORM ||
data->state.httpreq == HTTPREQ_POST_MIME) &&
!data->set.post301) {
if(HTTPREQ_IS_POST(data) && !data->set.post301) {
http_switch_to_get(data, 301);
switch_to_get = TRUE;
}
@ -1348,10 +1350,7 @@ CURLcode Curl_http_follow(struct Curl_easy *data, const char *newurl,
* This behavior is forbidden by RFC1945 and the obsolete RFC2616, and
* can be overridden with CURLOPT_POSTREDIR.
*/
if((data->state.httpreq == HTTPREQ_POST ||
data->state.httpreq == HTTPREQ_POST_FORM ||
data->state.httpreq == HTTPREQ_POST_MIME) &&
!data->set.post302) {
if(HTTPREQ_IS_POST(data) && !data->set.post302) {
http_switch_to_get(data, 302);
switch_to_get = TRUE;
}
@ -1361,13 +1360,8 @@ CURLcode Curl_http_follow(struct Curl_easy *data, const char *newurl,
/* 'See Other' location is not the resource but a substitute for the
* resource. In this case we switch the method to GET/HEAD, unless the
* method is POST and the user specified to keep it as POST.
* https://github.com/curl/curl/issues/5237#issuecomment-614641049
*/
if(data->state.httpreq != HTTPREQ_GET &&
((data->state.httpreq != HTTPREQ_POST &&
data->state.httpreq != HTTPREQ_POST_FORM &&
data->state.httpreq != HTTPREQ_POST_MIME) ||
!data->set.post303)) {
if(!HTTPREQ_IS_POST(data) || !data->set.post303) {
http_switch_to_get(data, 303);
switch_to_get = TRUE;
}

View File

@ -241,7 +241,7 @@ test1941 test1942 test1943 test1944 test1945 test1946 test1947 test1948 \
test1955 test1956 test1957 test1958 test1959 test1960 test1964 test1965 \
\
test1970 test1971 test1972 test1973 test1974 test1975 test1976 test1977 \
test1978 test1979 test1980 test1981 test1982 \
test1978 test1979 test1980 test1981 test1982 test1983 test1984 \
\
test2000 test2001 test2002 test2003 test2004 test2005 test2006 test2007 \
test2008 \

71
tests/data/test1983 Normal file
View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
HTTP DELETE
followlocation
</keywords>
</info>
# Server-side
<reply>
<data>
HTTP/1.1 303 OK swsclose
Location: moo.html%AMPtestcase=/%TESTNUMBER0002
Date: Tue, 09 Nov 2010 14:49:00 GMT
Connection: close
</data>
<data2>
HTTP/1.1 200 OK swsclose
Location: this should be ignored
Date: Tue, 09 Nov 2010 14:49:00 GMT
Connection: close
body
</data2>
<datacheck>
HTTP/1.1 303 OK swsclose
Location: moo.html%AMPtestcase=/%TESTNUMBER0002
Date: Tue, 09 Nov 2010 14:49:00 GMT
Connection: close
HTTP/1.1 200 OK swsclose
Location: this should be ignored
Date: Tue, 09 Nov 2010 14:49:00 GMT
Connection: close
body
</datacheck>
</reply>
# Client-side
<client>
<server>
http
</server>
<name>
HTTP DELETE with --follow and 303 redirect
</name>
<command>
http://%HOSTIP:%HTTPPORT/blah/%TESTNUMBER --follow -X DELETE
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<protocol crlf="yes">
DELETE /blah/%TESTNUMBER HTTP/1.1
Host: %HOSTIP:%HTTPPORT
User-Agent: curl/%VERSION
Accept: */*
GET /blah/moo.html%AMPtestcase=/%TESTNUMBER0002 HTTP/1.1
Host: %HOSTIP:%HTTPPORT
User-Agent: curl/%VERSION
Accept: */*
</protocol>
</verify>
</testcase>

76
tests/data/test1984 Normal file
View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
HTTP PUT
followlocation
</keywords>
</info>
# Server-side
<reply>
<data>
HTTP/1.1 303 OK swsclose
Location: moo.html%AMPtestcase=/%TESTNUMBER0002
Date: Tue, 09 Nov 2010 14:49:00 GMT
Connection: close
</data>
<data2>
HTTP/1.1 200 OK swsclose
Location: this should be ignored
Date: Tue, 09 Nov 2010 14:49:00 GMT
Connection: close
body
</data2>
<datacheck>
HTTP/1.1 303 OK swsclose
Location: moo.html%AMPtestcase=/%TESTNUMBER0002
Date: Tue, 09 Nov 2010 14:49:00 GMT
Connection: close
HTTP/1.1 200 OK swsclose
Location: this should be ignored
Date: Tue, 09 Nov 2010 14:49:00 GMT
Connection: close
body
</datacheck>
</reply>
# Client-side
<client>
<server>
http
</server>
<name>
HTTP PUT with 303 redirect and --follow
</name>
<command>
http://%HOSTIP:%HTTPPORT/blah/%TESTNUMBER --follow -T %LOGDIR/upload
</command>
<file name="%LOGDIR/upload">
Except for responses to a HEAD request, the representation of a 303 response ought to contain a short hypertext note with a hyperlink to the same URI reference provided in the Location header field.
</file>
</client>
# Verify data after the test has been "shot"
<verify>
<protocol crlf="headers">
PUT /blah/%TESTNUMBER HTTP/1.1
Host: %HOSTIP:%HTTPPORT
User-Agent: curl/%VERSION
Accept: */*
Content-Length: 199
Except for responses to a HEAD request, the representation of a 303 response ought to contain a short hypertext note with a hyperlink to the same URI reference provided in the Location header field.
GET /blah/moo.html%AMPtestcase=/%TESTNUMBER0002 HTTP/1.1
Host: %HOSTIP:%HTTPPORT
User-Agent: curl/%VERSION
Accept: */*
</protocol>
</verify>
</testcase>