Merge pull request #22237 from hrydgard/naett-cancel

http: Make Cancel() actually stop an HTTPS transfer
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-09-17 16:00:12 -06:00
commit bec55c61d1
10 files changed
+209 -26

No files matched your search

+19
View File
@@ -100,3 +100,22 @@ Keep this list up to date - it's what makes it possible to move to a newer upstr
- `src/naett_osx.c`: `invalidateAndCancel` returns before the session lets go of its delegate, so
the delegate's back pointer to the response is cleared first, and `didReceiveData` checks it
(as `didCompleteWithError` already did).
- `src/naett_osx.c` / `src/naett_android.c`: both threw away the body writer's return value.
Windows and Linux already treat a short write as a failed request - it's how the default
writer reports it couldn't grow, and how we cancel a transfer - so on those two a short write
silently truncated the body and still looked like a success. Apple also cancels the task, or
the data just keeps arriving.
- `naett.h`: documented what the body writer's return value means, since three of the four
backends' behaviour depends on it.
- `src/naett_win.c`: a short write from the body writer marked the request complete and then
queued another read anyway, so the transfer carried on and WinHTTP kept writing into a
response the caller was by then free to close. It stops there now, and the callback returns
early for anything raised after the request is complete.
- `src/naett_core.c`: `naettMake` only asserted that the request wasn't NULL, and the request
constructors return NULL when the platform can't set one up - a URL it can't parse, say. In a
release build that walked into a null dereference. Returns NULL instead.
- `src/naett_osx.c`: the delegate callbacks ignore a response that's already complete, so a
cancellation we asked for doesn't overwrite the error that caused it.
- `src/naett_core.c` / `naett.h`: `naettInit` asserted it was only ever called once, so a caller
that can be entered more than again needed a flag purely to guard it. A repeat call is a no-op
now, the way `WSAStartup` behaves, and `net::Init` dropped its `g_naettInitialized`.
+9
View File
@@ -17,6 +17,10 @@ typedef void* naettInitData;
/**
* @brief Global init method.
* Call to initialize the library.
*
* PPSSPP: calling this more than once is fine - everything after the first call does nothing,
* including the platform setup, so the init data from the first call is the one that sticks.
* Not thread safe; call it during startup, before anything else can reach the library.
*/
void naettInit(naettInitData initThing);
@@ -41,6 +45,11 @@ naettOption* naettBody(const char* body, int size);
// Sets a request body reader.
naettOption* naettBodyReader(naettReadFunc reader, void* userData);
// Sets a response body writer.
//
// PPSSPP: the writer is handed each chunk as it arrives, on whichever thread the backend runs
// its transfer on, and must return the number of bytes it took. Returning anything else fails
// the request with naettReadError and stops the transfer - which is both how the default writer
// reports that it couldn't grow its buffer, and how a caller cancels one in progress.
naettOption* naettBodyWriter(naettWriteFunc writer, void* userData);
// Sets connection timeout in milliseconds.
naettOption* naettTimeout(int milliSeconds);
+8 -1
View File
@@ -297,7 +297,14 @@ static void* processRequest(void* data) {
break;
} else if (bytesRead > 0) {
(*env)->GetByteArrayRegion(env, buffer, 0, bytesRead, (jbyte*) byteBuffer);
req->options.bodyWriter(byteBuffer, bytesRead, req->options.bodyWriterData);
// PPSSPP: a short write from the body writer fails the request - it's how a caller
// aborts, and how the default writer reports that it couldn't grow. Upstream ignored
// it, so the body was silently truncated and still reported as a success.
int written = req->options.bodyWriter(byteBuffer, bytesRead, req->options.bodyWriterData);
if (written != bytesRead) {
res->code = naettReadError;
goto finally;
}
res->totalBytesRead += bytesRead;
}
} while (!res->closeRequested);
+12 -1
View File
@@ -137,7 +137,12 @@ static void applyOptionParams(InternalRequest* req, InternalOption* option) {
// Public API
void naettInit(naettInitData initData) {
assert(!initialized);
// PPSSPP: upstream asserted that this was the first call. Callers that can be entered more
// than once then need a flag of their own purely to guard this, which is bookkeeping the
// library may as well do itself - so a repeat call is a no-op instead.
if (initialized) {
return;
}
naettPlatformInit(initData);
initialized = 1;
}
@@ -310,6 +315,12 @@ naettReq* naettRequestWithOptions(const char* url, int numOptions, const naettOp
naettRes* naettMake(naettReq* request) {
assert(initialized);
assert(request != NULL);
// PPSSPP: naettRequest* returns NULL when the platform can't set the request up - a URL it
// can't parse, most likely - and the assert above is compiled out in release, so this used to
// walk straight into a null dereference.
if (request == NULL) {
return NULL;
}
InternalRequest* req = (InternalRequest*)request;
naettAlloc(InternalResponse, res);
+19 -2
View File
@@ -100,7 +100,8 @@ void didReceiveData(id self, SEL _sel, id session, id dataTask, id data) {
object_getInstanceVariable(self, "response", (void**)&res);
// PPSSPP: didComplete already checked this; this one didn't, and the delegate outlives the
// response when a session is invalidated.
if (res == NULL) {
if (res == NULL || res->complete) {
// PPSSPP: once complete, the caller may already be closing this - don't touch it further.
release(p);
return;
}
@@ -153,7 +154,18 @@ void didReceiveData(id self, SEL _sel, id session, id dataTask, id data) {
const void* bytes = objc_msgSend_t(const void*)(data, sel("bytes"));
NSUInteger length = objc_msgSend_t(NSUInteger)(data, sel("length"));
res->request->options.bodyWriter(bytes, (int)length, res->request->options.bodyWriterData);
// PPSSPP: a writer that doesn't take everything is failing the request - that's how a caller
// aborts a transfer, and how the default writer reports that it couldn't grow. Upstream threw
// the result away here, so a short write silently truncated the body and still looked like a
// success. Cancel the task too, or the data just keeps coming.
int written = res->request->options.bodyWriter(bytes, (int)length, res->request->options.bodyWriterData);
if (written != (int)length) {
res->code = naettReadError;
res->complete = 1;
objc_msgSend_void(dataTask, sel("cancel"));
release(p);
return;
}
res->totalBytesRead += (int)length;
release(p);
@@ -163,6 +175,11 @@ static void didComplete(id self, SEL _sel, id session, id dataTask, id error) {
InternalResponse* res = NULL;
object_getInstanceVariable(self, "response", (void**)&res);
if (res != NULL) {
// PPSSPP: if we already failed the request - a writer that wouldn't take the data, say -
// that's the reason we want reported, not the cancellation it caused.
if (res->complete) {
return;
}
if (error != nil) {
res->code = naettConnectionError;
}
+15
View File
@@ -111,6 +111,13 @@ static void CALLBACK
callback(HINTERNET request, DWORD_PTR context, DWORD status, LPVOID statusInformation, DWORD statusInfoLength) {
InternalResponse* res = (InternalResponse*)context;
// PPSSPP: once we've reported the request as complete the caller is free to close it, so
// don't start any more I/O against it - anything still in flight would be writing into a
// response that's being freed.
if (res->complete) {
return;
}
switch (status) {
case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE: {
// PPSSPP: the sizing call only fills in bufSize when it fails with
@@ -177,9 +184,17 @@ callback(HINTERNET request, DWORD_PTR context, DWORD status, LPVOID statusInform
size_t bytesRead = statusInfoLength;
InternalRequest* req = res->request;
if (req == NULL) {
return;
}
if (req->options.bodyWriter(res->buffer, (int)bytesRead, req->options.bodyWriterData) != bytesRead) {
// PPSSPP: the writer failed the request - it couldn't grow, or the caller is
// cancelling. Upstream marked it complete and then queued another read anyway,
// which both kept the transfer running and left WinHTTP writing into a response
// the caller was by then free to close.
res->code = naettReadError;
res->complete = 1;
break;
}
res->totalBytesRead += (int)bytesRead;
// PPSSPP: bytesLeft is unsigned, so a read longer than announced used to wrap it