diff --git a/lib/CurlMimePart.ts b/lib/CurlMimePart.ts index d5e9d8e3..cc3ff3e5 100644 --- a/lib/CurlMimePart.ts +++ b/lib/CurlMimePart.ts @@ -24,7 +24,7 @@ export interface MimeDataCallbacks { * * @remarks * When `CurlReadFunc.Pause` is returned, the transfer will be paused until it is - * explicitly resumed by calling `handle.pause(handle.pauseFlags & ~CurlPause.Recv)`. + * explicitly resumed by calling `handle.pause(handle.pauseFlags & ~CurlPause.Send)`. * When `CurlReadFunc.Abort` is returned, the transfer will be aborted. * * @example @@ -360,8 +360,9 @@ declare class CurlMimePart { * `CurlReadFunc.Pause`, and the `unpause` callback is invoked when data becomes * available to resume the transfer. * - * The `unpause` function should unpause the curl handle's receive operation, typically - * by calling `handle.pause(handle.pauseFlags & ~CurlPause.Recv)`. + * The `unpause` function should unpause the curl handle's send operation (mime upload + * data is sent via the read callback), typically by calling + * `handle.pause(handle.pauseFlags & ~CurlPause.Send)`. * * For very large files, consider using {@link setFileData} instead, as it streams * directly from disk without going through Node.js streams. @@ -380,7 +381,7 @@ declare class CurlMimePart { * .addPart() * .setName('document') * .setDataStream(stream, () => { - * curl.pause(curl.handle.pauseFlags & ~CurlPause.Recv) + * curl.pause(curl.handle.pauseFlags & ~CurlPause.Send) * }) * .setType('text/plain') * ``` @@ -402,7 +403,7 @@ declare class CurlMimePart { * .setName('document') * .setDataStream( * stream, - * () => curl.pause(curl.handle.pauseFlags & ~CurlPause.Recv), + * () => curl.pause(curl.handle.pauseFlags & ~CurlPause.Send), * size * ) * ``` @@ -424,21 +425,36 @@ CurlMimePart.prototype.setDataStream = function ( ): typeof CurlMimePart.prototype { let streamEnded = false let streamError: Error | null = null + let paused = false + + // Defer unpause to the next event loop iteration to avoid calling + // curl_easy_pause() while libcurl is still processing the READFUNC_PAUSE + // return value from the read callback. Without this, the synchronous + // unpause can re-enter libcurl and cause a hang (observed on Linux). + // This matches the pattern used by setUploadStream in Curl.ts. + const deferredUnpause = () => { + if (paused) { + paused = false + setImmediate(() => { + unpause() + }) + } + } const onReadable = () => { - unpause() + deferredUnpause() } const onEnd = () => { streamEnded = true - unpause() + deferredUnpause() cleanup() } const onError = (err: Error) => { streamError = err streamEnded = true - unpause() + deferredUnpause() cleanup() } @@ -473,6 +489,7 @@ CurlMimePart.prototype.setDataStream = function ( if (streamEnded) { return null } + paused = true return CurlReadFunc.Pause } diff --git a/lib/Easy.ts b/lib/Easy.ts index d8dcac39..9dbabd30 100644 --- a/lib/Easy.ts +++ b/lib/Easy.ts @@ -745,7 +745,8 @@ const Easy = bindings.Easy as Easy * @remarks * For stream-based parts, you must provide the unpause callback that will be * called when more data is available. The callback should unpause the transfer - * using `handle.pause(handle.pauseFlags & ~CurlPause.Recv)`. + * using `handle.pause(handle.pauseFlags & ~CurlPause.Send)` (mime upload data + * is sent via the read callback, so it pauses SEND, not RECV). * * Available since libcurl 7.56.0. * @@ -774,7 +775,7 @@ const Easy = bindings.Easy as Easy * name: 'logfile', * stream: createReadStream('/path/to/log.txt'), * unpause: () => { - * easy.pause(easy.pauseFlags & ~CurlPause.Recv) + * easy.pause(easy.pauseFlags & ~CurlPause.Send) * }, * size: 12345 * }, @@ -832,7 +833,7 @@ Easy.prototype.setMimePost = function ( part.setDataStream( partSpec.stream, () => { - this.pause(this.pauseFlags & ~CurlPause.Recv) + this.pause(this.pauseFlags & ~CurlPause.Send) }, partSpec.size, ) diff --git a/src/CurlMime.cc b/src/CurlMime.cc index 5ce1e328..be2b4e12 100644 --- a/src/CurlMime.cc +++ b/src/CurlMime.cc @@ -595,7 +595,14 @@ size_t CurlMimePart::StaticReadCallback(char* buffer, size_t size, size_t nitems } if (result.IsNumber()) { - return result.As().Int32Value(); + int32_t returnValue = result.As().Int32Value(); + // Track pause state so isPausedSend reflects reality. + // The mime data callback pauses SEND (it supplies upload data), + // matching the behavior of Easy::ReadFunction. + if (returnValue == CURL_READFUNC_PAUSE) { + part->easy->pauseState |= CURLPAUSE_SEND; + } + return static_cast(returnValue); } // Invalid return type diff --git a/test/curl/CurlMime.spec.ts b/test/curl/CurlMime.spec.ts index 235c7f84..2606e610 100644 --- a/test/curl/CurlMime.spec.ts +++ b/test/curl/CurlMime.spec.ts @@ -813,8 +813,8 @@ describe.runIf(Curl.isVersionGreaterOrEqualThan(7, 56, 0))('CurlMime', () => { .addPart() .setName('stream_field') .setDataStream(stream, () => { - if (curl.handle.isPausedRecv) { - curl.pause(curl.handle.pauseFlags & ~CurlPause.Recv) + if (curl.handle.isPausedSend) { + curl.pause(curl.handle.pauseFlags & ~CurlPause.Send) } }) @@ -863,8 +863,8 @@ describe.runIf(Curl.isVersionGreaterOrEqualThan(7, 56, 0))('CurlMime', () => { .setDataStream( stream, () => { - if (curl.handle.isPausedRecv) { - curl.pause(curl.handle.pauseFlags & ~CurlPause.Recv) + if (curl.handle.isPausedSend) { + curl.pause(curl.handle.pauseFlags & ~CurlPause.Send) } }, testData.length, @@ -913,8 +913,8 @@ describe.runIf(Curl.isVersionGreaterOrEqualThan(7, 56, 0))('CurlMime', () => { .addPart() .setName('buffer_stream') .setDataStream(stream, () => { - if (curl.handle.isPausedRecv) { - curl.pause(curl.handle.pauseFlags & ~CurlPause.Recv) + if (curl.handle.isPausedSend) { + curl.pause(curl.handle.pauseFlags & ~CurlPause.Send) } }) @@ -961,8 +961,8 @@ describe.runIf(Curl.isVersionGreaterOrEqualThan(7, 56, 0))('CurlMime', () => { .addPart() .setName('chained_stream') .setDataStream(stream, () => { - if (curl.handle.isPausedRecv) { - curl.pause(curl.handle.pauseFlags & ~CurlPause.Recv) + if (curl.handle.isPausedSend) { + curl.pause(curl.handle.pauseFlags & ~CurlPause.Send) } }) .setType('text/plain') @@ -1012,8 +1012,8 @@ describe.runIf(Curl.isVersionGreaterOrEqualThan(7, 56, 0))('CurlMime', () => { .addPart() .setName('chunked_stream') .setDataStream(stream, () => { - if (curl.handle.isPausedRecv) { - curl.pause(curl.handle.pauseFlags & ~CurlPause.Recv) + if (curl.handle.isPausedSend) { + curl.pause(curl.handle.pauseFlags & ~CurlPause.Send) } }) @@ -1058,8 +1058,8 @@ describe.runIf(Curl.isVersionGreaterOrEqualThan(7, 56, 0))('CurlMime', () => { .addPart() .setName('empty_stream') .setDataStream(stream, () => { - if (curl.handle.isPausedRecv) { - curl.pause(curl.handle.pauseFlags & ~CurlPause.Recv) + if (curl.handle.isPausedSend) { + curl.pause(curl.handle.pauseFlags & ~CurlPause.Send) } })