Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions lib/CurlMimePart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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')
* ```
Expand All @@ -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
* )
* ```
Expand All @@ -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()
}

Expand Down Expand Up @@ -473,6 +489,7 @@ CurlMimePart.prototype.setDataStream = function (
if (streamEnded) {
return null
}
paused = true
return CurlReadFunc.Pause
}

Expand Down
7 changes: 4 additions & 3 deletions lib/Easy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
* },
Expand Down Expand Up @@ -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,
)
Expand Down
9 changes: 8 additions & 1 deletion src/CurlMime.cc
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,14 @@ size_t CurlMimePart::StaticReadCallback(char* buffer, size_t size, size_t nitems
}

if (result.IsNumber()) {
return result.As<Napi::Number>().Int32Value();
int32_t returnValue = result.As<Napi::Number>().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<size_t>(returnValue);
}

// Invalid return type
Expand Down
24 changes: 12 additions & 12 deletions test/curl/CurlMime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}
})

Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)
}
})

Expand Down Expand Up @@ -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)
}
})

Expand Down
Loading