[php-src] master: ext/curl: add CURLOPT_SEEKFUNCTION

Graham Campbell via Ayesh Karunaratne <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Graham Campbell (GrahamCampbell)
Committer: Ayesh Karunaratne (Ayesh)
Date: 2026-07-13T00:19:20+07:00

Commit: https://github.com/php/php-src/commit/93fa5f3c882e5b51342fa0674d25d5d29d7a0546
Raw diff: https://github.com/php/php-src/commit/93fa5f3c882e5b51342fa0674d25d5d29d7a0546.diff

ext/curl: add CURLOPT_SEEKFUNCTION

Changed paths:
  A  ext/curl/tests/curl_copy_handle_seek.phpt
  A  ext/curl/tests/curl_seekfunction.phpt
  A  ext/curl/tests/curl_seekfunction_error.phpt
  M  NEWS
  M  UPGRADING
  M  ext/curl/curl.stub.php
  M  ext/curl/curl_arginfo.h
  M  ext/curl/curl_private.h
  M  ext/curl/interface.c
  M  ext/curl/tests/curl_setopt_callables.phpt
  M  ext/curl/tests/responder/get.inc


Diff:

diff --git a/NEWS b/NEWS
index 190c6c16a935..4be8d1e554c5 100644
--- a/NEWS
+++ b/NEWS
@@ -14,6 +14,12 @@ PHP                                                                        NEWS
   . Fixed bug GH-22602 (gregoriantojd() and juliantojd() integer overflow with
     INT_MAX year). (arshidkv12)
 
+- Curl:
+  . Added CURLOPT_SEEKFUNCTION and the CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL
+    and CURL_SEEKFUNC_CANTSEEK constants, letting libcurl rewind a streamed
+    request body to resend it on a redirect, multi-pass authentication or a
+    retried reused connection. (GrahamCampbell)
+
 - Date:
   . Update timelib to 2022.17. (Derick)
   . Fixed bug GH-19803 (Parsing a string with a single white space does create
diff --git a/UPGRADING b/UPGRADING
index 031b6751d853..a809687d1961 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -238,6 +238,12 @@ PHP 8.6 UPGRADE NOTES
     This value can also be obtained by passing CURLINFO_SIZE_DELIVERED as the
     $option parameter.
     Requires libcurl 8.20.0 or later.
+  . Added CURLOPT_SEEKFUNCTION to register a callback that repositions a
+    streamed request body so libcurl can rewind and resend it on a redirect,
+    multi-pass authentication, or a retried reused connection instead of
+    failing with CURLE_SEND_FAIL_REWIND. The callback receives the CurlHandle,
+    offset and origin, and must return one of CURL_SEEKFUNC_OK,
+    CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK.
 
 - Fileinfo:
   . finfo_file() now works with remote streams.
@@ -467,6 +473,10 @@ PHP 8.6 UPGRADE NOTES
 
 - Curl:
   . CURLINFO_SIZE_DELIVERED (libcurl >= 8.20.0).
+  . CURLOPT_SEEKFUNCTION.
+  . CURL_SEEKFUNC_OK.
+  . CURL_SEEKFUNC_FAIL.
+  . CURL_SEEKFUNC_CANTSEEK.
 
 - Sockets:
   . TCP_USER_TIMEOUT (Linux only).
diff --git a/ext/curl/curl.stub.php b/ext/curl/curl.stub.php
index d3bd58510e40..70e87cc9b146 100644
--- a/ext/curl/curl.stub.php
+++ b/ext/curl/curl.stub.php
@@ -343,6 +343,11 @@
  * @cvalue CURLOPT_RETURNTRANSFER
  */
 const CURLOPT_RETURNTRANSFER = UNKNOWN;
+/**
+ * @var int
+ * @cvalue CURLOPT_SEEKFUNCTION
+ */
+const CURLOPT_SEEKFUNCTION = UNKNOWN;
 /**
  * @var int
  * @cvalue CURLOPT_SHARE
@@ -1788,6 +1793,21 @@
  * @cvalue CURL_READFUNC_PAUSE
  */
 const CURL_READFUNC_PAUSE = UNKNOWN;
+/**
+ * @var int
+ * @cvalue CURL_SEEKFUNC_OK
+ */
+const CURL_SEEKFUNC_OK = UNKNOWN;
+/**
+ * @var int
+ * @cvalue CURL_SEEKFUNC_FAIL
+ */
+const CURL_SEEKFUNC_FAIL = UNKNOWN;
+/**
+ * @var int
+ * @cvalue CURL_SEEKFUNC_CANTSEEK
+ */
+const CURL_SEEKFUNC_CANTSEEK = UNKNOWN;
 /**
  * @var int
  * @cvalue CURL_WRITEFUNC_PAUSE
diff --git a/ext/curl/curl_arginfo.h b/ext/curl/curl_arginfo.h
index 6a05b73572cb..f2929f60c4e2 100644
--- a/ext/curl/curl_arginfo.h
+++ b/ext/curl/curl_arginfo.h
@@ -1,5 +1,5 @@
 /* This is a generated file, edit curl.stub.php instead.
- * Stub hash: 1ecf692ba8494e7b486986c0170686c33768deb5 */
+ * Stub hash: d55adb230c533f4dde05e95759477dd9e1dd6efb */
 
 ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_curl_close, 0, 1, IS_VOID, 0)
 	ZEND_ARG_OBJ_INFO(0, handle, CurlHandle, 0)
@@ -294,6 +294,7 @@ static void register_curl_symbols(int module_number)
 	REGISTER_LONG_CONSTANT("CURLOPT_REFERER", CURLOPT_REFERER, CONST_PERSISTENT);
 	REGISTER_LONG_CONSTANT("CURLOPT_RESUME_FROM", CURLOPT_RESUME_FROM, CONST_PERSISTENT);
 	REGISTER_LONG_CONSTANT("CURLOPT_RETURNTRANSFER", CURLOPT_RETURNTRANSFER, CONST_PERSISTENT);
+	REGISTER_LONG_CONSTANT("CURLOPT_SEEKFUNCTION", CURLOPT_SEEKFUNCTION, CONST_PERSISTENT);
 	REGISTER_LONG_CONSTANT("CURLOPT_SHARE", CURLOPT_SHARE, CONST_PERSISTENT);
 	REGISTER_LONG_CONSTANT("CURLOPT_SSLCERT", CURLOPT_SSLCERT, CONST_PERSISTENT);
 	REGISTER_LONG_CONSTANT("CURLOPT_SSLCERTPASSWD", CURLOPT_SSLCERTPASSWD, CONST_PERSISTENT);
@@ -574,6 +575,9 @@ static void register_curl_symbols(int module_number)
 	REGISTER_LONG_CONSTANT("CURLPAUSE_SEND", CURLPAUSE_SEND, CONST_PERSISTENT);
 	REGISTER_LONG_CONSTANT("CURLPAUSE_SEND_CONT", CURLPAUSE_SEND_CONT, CONST_PERSISTENT);
 	REGISTER_LONG_CONSTANT("CURL_READFUNC_PAUSE", CURL_READFUNC_PAUSE, CONST_PERSISTENT);
+	REGISTER_LONG_CONSTANT("CURL_SEEKFUNC_OK", CURL_SEEKFUNC_OK, CONST_PERSISTENT);
+	REGISTER_LONG_CONSTANT("CURL_SEEKFUNC_FAIL", CURL_SEEKFUNC_FAIL, CONST_PERSISTENT);
+	REGISTER_LONG_CONSTANT("CURL_SEEKFUNC_CANTSEEK", CURL_SEEKFUNC_CANTSEEK, CONST_PERSISTENT);
 	REGISTER_LONG_CONSTANT("CURL_WRITEFUNC_PAUSE", CURL_WRITEFUNC_PAUSE, CONST_PERSISTENT);
 	REGISTER_LONG_CONSTANT("CURLPROXY_SOCKS4A", CURLPROXY_SOCKS4A, CONST_PERSISTENT);
 	REGISTER_LONG_CONSTANT("CURLPROXY_SOCKS5_HOSTNAME", CURLPROXY_SOCKS5_HOSTNAME, CONST_PERSISTENT);
diff --git a/ext/curl/curl_private.h b/ext/curl/curl_private.h
index 77b0628ee42a..25cc37b6e458 100644
--- a/ext/curl/curl_private.h
+++ b/ext/curl/curl_private.h
@@ -74,6 +74,7 @@ typedef struct {
 	php_curl_write    *write_header;
 	php_curl_read     *read;
 	zval               std_err;
+	zend_fcall_info_cache seek;
 	zend_fcall_info_cache progress;
 	zend_fcall_info_cache xferinfo;
 	zend_fcall_info_cache fnmatch;
diff --git a/ext/curl/interface.c b/ext/curl/interface.c
index 09c7009068b6..b7ec015abf62 100644
--- a/ext/curl/interface.c
+++ b/ext/curl/interface.c
@@ -456,6 +456,10 @@ static HashTable *curl_get_gc(zend_object *object, zval **table, int *n)
 		zend_get_gc_buffer_add_zval(gc_buffer, &curl->handlers.write_header->stream);
 	}
 
+	if (ZEND_FCC_INITIALIZED(curl->handlers.seek)) {
+		zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.seek);
+	}
+
 	if (ZEND_FCC_INITIALIZED(curl->handlers.progress)) {
 		zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.progress);
 	}
@@ -831,6 +835,52 @@ static size_t curl_read(char *data, size_t size, size_t nmemb, void *ctx)
 }
 /* }}} */
 
+/* {{{ curl_seek */
+static int curl_seek(void *clientp, curl_off_t offset, int origin)
+{
+	php_curl *ch = (php_curl *)clientp;
+	int rval = CURL_SEEKFUNC_CANTSEEK; /* safe default if unset or the callback misbehaves */
+
+#if PHP_CURL_DEBUG
+	fprintf(stderr, "curl_seek() called\n");
+	fprintf(stderr, "clientp = %x, offset = %ld, origin = %d\n", clientp, offset, origin);
+#endif
+	if (!ZEND_FCC_INITIALIZED(ch->handlers.seek)) {
+		return rval;
+	}
+
+	zval args[3];
+	zval retval;
+
+	GC_ADDREF(&ch->std);
+	ZVAL_OBJ(&args[0], &ch->std);
+	ZVAL_LONG(&args[1], offset);
+	ZVAL_LONG(&args[2], origin);
+
+	ch->in_callback = true;
+	zend_call_known_fcc(&ch->handlers.seek, &retval, /* param_count */ 3, args, /* named_params */ NULL);
+	ch->in_callback = false;
+
+	if (!Z_ISUNDEF(retval)) {
+		_php_curl_verify_handlers(ch, /* reporterror */ true);
+		if (Z_TYPE(retval) == IS_LONG) {
+			zend_long retval_long = Z_LVAL(retval);
+			if (retval_long == CURL_SEEKFUNC_OK || retval_long == CURL_SEEKFUNC_FAIL || retval_long == CURL_SEEKFUNC_CANTSEEK) {
+				rval = retval_long;
+			} else {
+				zend_value_error("The CURLOPT_SEEKFUNCTION callback must return one of CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK");
+			}
+		} else {
+			zend_type_error("The CURLOPT_SEEKFUNCTION callback must return one of CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK");
+		}
+		zval_ptr_dtor(&retval);
+	}
+
+	zval_ptr_dtor(&args[0]);
+	return rval;
+}
+/* }}} */
+
 /* {{{ curl_write_header */
 static size_t curl_write_header(char *data, size_t size, size_t nmemb, void *ctx)
 {
@@ -1038,6 +1088,7 @@ void init_curl_handle(php_curl *ch)
 	ch->handlers.write = ecalloc(1, sizeof(php_curl_write));
 	ch->handlers.write_header = ecalloc(1, sizeof(php_curl_write));
 	ch->handlers.read = ecalloc(1, sizeof(php_curl_read));
+	ch->handlers.seek = empty_fcall_info_cache;
 	ch->handlers.progress = empty_fcall_info_cache;
 	ch->handlers.xferinfo = empty_fcall_info_cache;
 	ch->handlers.fnmatch = empty_fcall_info_cache;
@@ -1208,6 +1259,7 @@ void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source)
 	curl_easy_setopt(ch->cp, CURLOPT_WRITEHEADER,       (void *) ch);
 	curl_easy_setopt(ch->cp, CURLOPT_DEBUGDATA,         (void *) ch);
 
+	php_curl_copy_fcc_with_option(ch, CURLOPT_SEEKDATA, &ch->handlers.seek, &source->handlers.seek);
 	php_curl_copy_fcc_with_option(ch, CURLOPT_PROGRESSDATA, &ch->handlers.progress, &source->handlers.progress);
 	php_curl_copy_fcc_with_option(ch, CURLOPT_XFERINFODATA, &ch->handlers.xferinfo, &source->handlers.xferinfo);
 	php_curl_copy_fcc_with_option(ch, CURLOPT_FNMATCH_DATA, &ch->handlers.fnmatch, &source->handlers.fnmatch);
@@ -1577,6 +1629,7 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue
 		HANDLE_CURL_OPTION_CALLABLE_PHP_CURL_USER(ch, CURLOPT_HEADER, write_header, PHP_CURL_IGNORE);
 		HANDLE_CURL_OPTION_CALLABLE_PHP_CURL_USER(ch, CURLOPT_READ, read, PHP_CURL_DIRECT);
 
+		HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_SEEK, handlers.seek, curl_seek);
 		HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_PROGRESS, handlers.progress, curl_progress);
 		HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_XFERINFO, handlers.xferinfo, curl_xferinfo);
 		HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_FNMATCH_, handlers.fnmatch, curl_fnmatch);
@@ -2794,6 +2847,9 @@ static void curl_free_obj(zend_object *object)
 	efree(ch->handlers.write_header);
 	efree(ch->handlers.read);
 
+	if (ZEND_FCC_INITIALIZED(ch->handlers.seek)) {
+		zend_fcc_dtor(&ch->handlers.seek);
+	}
 	if (ZEND_FCC_INITIALIZED(ch->handlers.progress)) {
 		zend_fcc_dtor(&ch->handlers.progress);
 	}
@@ -2878,6 +2934,10 @@ static void _php_curl_reset_handlers(php_curl *ch)
 		ZVAL_UNDEF(&ch->handlers.std_err);
 	}
 
+	if (ZEND_FCC_INITIALIZED(ch->handlers.seek)) {
+		zend_fcc_dtor(&ch->handlers.seek);
+	}
+
 	if (ZEND_FCC_INITIALIZED(ch->handlers.progress)) {
 		zend_fcc_dtor(&ch->handlers.progress);
 	}
diff --git a/ext/curl/tests/curl_copy_handle_seek.phpt b/ext/curl/tests/curl_copy_handle_seek.phpt
new file mode 100644
index 000000000000..c2a2f37bbe13
--- /dev/null
+++ b/ext/curl/tests/curl_copy_handle_seek.phpt
@@ -0,0 +1,44 @@
+--TEST--
+Test curl_copy_handle() with CURLOPT_SEEKFUNCTION
+--EXTENSIONS--
+curl
+--FILE--
+<?php
+include 'server.inc';
+$host = curl_cli_server_start();
+
+$body = 'Hello cURL seek!';
+$offset = 0;
+$seekCalls = 0;
+
+$ch = curl_init("{$host}/get.inc?test=redirect");
+curl_setopt($ch, CURLOPT_UPLOAD, true);
+curl_setopt($ch, CURLOPT_INFILESIZE, strlen($body));
+curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_setopt($ch, CURLOPT_READFUNCTION, function ($ch, $fd, int $length) use ($body, &$offset) {
+    $chunk = substr($body, $offset, $length);
+    $offset += strlen($chunk);
+    return $chunk;
+});
+curl_setopt($ch, CURLOPT_SEEKFUNCTION, function ($ch, int $position, int $origin) use (&$offset, &$seekCalls) {
+    if ($origin !== SEEK_SET) {
+        return CURL_SEEKFUNC_CANTSEEK;
+    }
+    $seekCalls++;
+    $offset = $position;
+    return CURL_SEEKFUNC_OK;
+});
+
+// The copied handle must inherit the seek callback; exercise it on the copy
+// after freeing the original.
+$ch2 = curl_copy_handle($ch);
+unset($ch);
+
+$response = curl_exec($ch2);
+var_dump($seekCalls > 0);
+var_dump(str_contains($response, $body));
+?>
+--EXPECT--
+bool(true)
+bool(true)
diff --git a/ext/curl/tests/curl_seekfunction.phpt b/ext/curl/tests/curl_seekfunction.phpt
new file mode 100644
index 000000000000..d2daa1e7d8dc
--- /dev/null
+++ b/ext/curl/tests/curl_seekfunction.phpt
@@ -0,0 +1,51 @@
+--TEST--
+CURLOPT_SEEKFUNCTION is called to rewind a streamed upload across a redirect
+--EXTENSIONS--
+curl
+--FILE--
+<?php
+include 'server.inc';
+$host = curl_cli_server_start();
+
+$body = 'Hello cURL seek!';
+$offset = 0;
+$seekCalls = 0;
+$argsChecked = false;
+
+$ch = curl_init("{$host}/get.inc?test=redirect");
+curl_setopt($ch, CURLOPT_UPLOAD, true);
+curl_setopt($ch, CURLOPT_INFILESIZE, strlen($body));
+curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+curl_setopt($ch, CURLOPT_READFUNCTION, function ($ch, $fd, int $length) use ($body, &$offset) {
+    $chunk = substr($body, $offset, $length);
+    $offset += strlen($chunk);
+    return $chunk;
+});
+curl_setopt($ch, CURLOPT_SEEKFUNCTION, function ($ch, int $position, int $origin) use (&$offset, &$seekCalls, &$argsChecked) {
+    if (!$argsChecked) {
+        $argsChecked = true;
+        var_dump($ch instanceof CurlHandle);
+        var_dump($position === 0);
+        var_dump($origin === SEEK_SET);
+    }
+    if ($origin !== SEEK_SET) {
+        return CURL_SEEKFUNC_CANTSEEK;
+    }
+    $seekCalls++;
+    $offset = $position;
+    return CURL_SEEKFUNC_OK;
+});
+
+$response = curl_exec($ch);
+// The seek callback must have been invoked to rewind the body for the resend,
+// and the resent body must have reached the redirect target intact.
+var_dump($seekCalls > 0);
+var_dump(str_contains($response, $body));
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+bool(true)
diff --git a/ext/curl/tests/curl_seekfunction_error.phpt b/ext/curl/tests/curl_seekfunction_error.phpt
new file mode 100644
index 000000000000..134e8115dc5f
--- /dev/null
+++ b/ext/curl/tests/curl_seekfunction_error.phpt
@@ -0,0 +1,77 @@
+--TEST--
+CURLOPT_SEEKFUNCTION callback error handling and option validation
+--EXTENSIONS--
+curl
+--FILE--
+<?php
+include 'server.inc';
+$host = curl_cli_server_start();
+
+// Drive a 307-redirect upload so libcurl invokes the seek callback to rewind
+// the body; $seek is the callback under test.
+function run_upload(string $host, callable $seek): void
+{
+    $offset = 0;
+    $body = 'Hello cURL seek!';
+    $ch = curl_init("{$host}/get.inc?test=redirect");
+    curl_setopt($ch, CURLOPT_UPLOAD, true);
+    curl_setopt($ch, CURLOPT_INFILESIZE, strlen($body));
+    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+    curl_setopt($ch, CURLOPT_READFUNCTION, function ($ch, $fd, int $length) use ($body, &$offset) {
+        $chunk = substr($body, $offset, $length);
+        $offset += strlen($chunk);
+        return $chunk;
+    });
+    curl_setopt($ch, CURLOPT_SEEKFUNCTION, $seek);
+    curl_exec($ch);
+}
+
+echo "Returning a non-int:\n";
+try {
+    run_upload($host, fn($ch, $offset, $origin) => 'not an int');
+} catch (\TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+echo "\nReturning an out-of-range int:\n";
+try {
+    run_upload($host, fn($ch, $offset, $origin) => 42);
+} catch (\ValueError $e) {
+    echo $e->getMessage(), "\n";
+}
+
+echo "\nThrowing from the callback:\n";
+try {
+    run_upload($host, function ($ch, $offset, $origin) {
+        throw new \RuntimeException('boom from seek');
+    });
+} catch (\RuntimeException $e) {
+    echo $e->getMessage(), "\n";
+}
+
+echo "\nSetting the callback to null:\n";
+var_dump(curl_setopt(curl_init(), CURLOPT_SEEKFUNCTION, null));
+
+echo "\nSetting a non-callable scalar:\n";
+try {
+    curl_setopt(curl_init(), CURLOPT_SEEKFUNCTION, 42);
+} catch (\TypeError $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Returning a non-int:
+The CURLOPT_SEEKFUNCTION callback must return one of CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK
+
+Returning an out-of-range int:
+The CURLOPT_SEEKFUNCTION callback must return one of CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK
+
+Throwing from the callback:
+boom from seek
+
+Setting the callback to null:
+bool(true)
+
+Setting a non-callable scalar:
+curl_setopt(): Argument #3 ($value) must be a valid callback for option CURLOPT_SEEKFUNCTION, no array or string given
diff --git a/ext/curl/tests/curl_setopt_callables.phpt b/ext/curl/tests/curl_setopt_callables.phpt
index aaa83102afac..9de8d6705710 100644
--- a/ext/curl/tests/curl_setopt_callables.phpt
+++ b/ext/curl/tests/curl_setopt_callables.phpt
@@ -27,6 +27,7 @@ testOption($ch, CURLOPT_FNMATCH_FUNCTION);
 testOption($ch, CURLOPT_WRITEFUNCTION);
 testOption($ch, CURLOPT_HEADERFUNCTION);
 testOption($ch, CURLOPT_READFUNCTION);
+testOption($ch, CURLOPT_SEEKFUNCTION);
 
 ?>
 --EXPECT--
@@ -42,3 +43,5 @@ TypeError: curl_setopt(): Argument #3 ($value) must be a valid callback for opti
 TypeError: curl_setopt_array(): Argument #2 ($options) must be a valid callback for option CURLOPT_HEADERFUNCTION, function "undefined" not found or invalid function name
 TypeError: curl_setopt(): Argument #3 ($value) must be a valid callback for option CURLOPT_READFUNCTION, function "undefined" not found or invalid function name
 TypeError: curl_setopt_array(): Argument #2 ($options) must be a valid callback for option CURLOPT_READFUNCTION, function "undefined" not found or invalid function name
+TypeError: curl_setopt(): Argument #3 ($value) must be a valid callback for option CURLOPT_SEEKFUNCTION, function "undefined" not found or invalid function name
+TypeError: curl_setopt_array(): Argument #2 ($options) must be a valid callback for option CURLOPT_SEEKFUNCTION, function "undefined" not found or invalid function name
diff --git a/ext/curl/tests/responder/get.inc b/ext/curl/tests/responder/get.inc
index c139c8c7d43a..2ef1e4a89dd3 100644
--- a/ext/curl/tests/responder/get.inc
+++ b/ext/curl/tests/responder/get.inc
@@ -46,6 +46,11 @@
     case 'method':
       echo $_SERVER['REQUEST_METHOD'];
       break;
+    case 'redirect':
+      // A 307 preserves the method and body, so libcurl must rewind the upload
+      // (via CURLOPT_SEEKFUNCTION) before resending it to the new location.
+      header('Location: /get.inc?test=input', true, 307);
+      break;
     default:
       echo "Hello World!\n";
       echo "Hello World!";
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.