diff --git a/docs/index.d.ts b/docs/index.d.ts index 4f21d66ad..d725cf88a 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -205,6 +205,15 @@ export interface HttpResponse { * If the total body size exceeds maxSize bytes, handler is called with null instead. */ collectBody(maxSize: number, handler: (fullBody: ArrayBuffer | null) => void) : HttpResponse; + /** collectJson collects the body like collectBody and parses it with V8's own JSON parser. + * The handler gets the value, or undefined when the body is over maxSize bytes or not valid JSON. */ + collectJson(maxSize: number, handler: (value: any) => void) : HttpResponse; + + /** Without a handler collectJson returns a Promise of the value. A body over maxSize bytes gets 413 and invalid JSON + * gets 400 from uWS itself, and on abort the promise never settles. So an async handler can await it with no + * onAborted and no try/catch, and write its response right after the await without cork. */ + collectJson(maxSize: number) : Promise; + /** Handler for reading HTTP request body data. V2. * Must be attached before performing any asynchronous operation, otherwise data may be lost. * You MUST copy the data of chunk if maxRemainingBodyLength is not 0n. We Neuter ArrayBuffers on return, making them zero length. diff --git a/src/HttpResponseWrapper.h b/src/HttpResponseWrapper.h index 86192280f..100063c2f 100644 --- a/src/HttpResponseWrapper.h +++ b/src/HttpResponseWrapper.h @@ -190,6 +190,109 @@ struct HttpResponseWrapper { } } + /* Parses with V8's own JSON parser, empty when the text is not valid JSON (the exception stays here) */ + static MaybeLocal parseJson(Isolate *isolate, std::string_view json) { + TryCatch tryCatch(isolate); + Local string; + if (json.length() > (size_t) String::kMaxLength || !String::NewFromUtf8(isolate, json.data(), NewStringType::kNormal, (int) json.length()).ToLocal(&string)) { + return {}; + } + return JSON::Parse(isolate->GetCurrentContext(), string); + } + + /* Takes integer maxSize and an optional function of the value. Collects the body like collectBody and parses it + * with JSON::Parse. The function gets the value, or undefined when the body is over maxSize or not valid JSON. + * Without a function it returns a Promise of the value: a body over maxSize gets a 413 and invalid JSON a 400 from + * here, and on abort the promise never settles, so an async handler needs neither onAborted nor try/catch */ + template + static void res_collectJson(const FunctionCallbackInfo &args) { + Isolate *isolate = args.GetIsolate(); + auto *res = getHttpResponse(args); + if (!res) { + return; + } + Local context = isolate->GetCurrentContext(); + size_t maxSize = (size_t) args[0]->NumberValue(context).ToChecked(); + + NoexceptPersistent callback; + NoexceptPersistent resolver; + NoexceptPersistent resObject; + if (args[1]->IsFunction()) { + callback.Reset(isolate, Local::Cast(args[1])); + args.GetReturnValue().Set(args.This()); + } else { + Local promiseResolver = Promise::Resolver::New(context).ToLocalChecked(); + resolver.Reset(isolate, promiseResolver); + resObject.Reset(isolate, args.This()); + + /* uWS terminates when a handler returns with no response and no abort handler, and an awaiting + * handler has neither. So attach one, unless there is already one: it only marks res invalid */ + if (!args.This()->GetInternalField(1).As()->IsFunction()) { + NoexceptPersistent abortedObject(isolate, args.This()); + res->onAborted([abortedObject = std::move(abortedObject), isolate]() { + HandleScope hs(isolate); + setInternalPointer(Local::New(isolate, abortedObject.p), nullptr); + }); + } + args.GetReturnValue().Set(promiseResolver->GetPromise()); + } + + std::unique_ptr> buffer; + bool overflow = false; + + res->onDataV2([callback = std::move(callback), resolver = std::move(resolver), resObject = std::move(resObject), buffer = std::move(buffer), overflow, maxSize, isolate, res](std::string_view data, uint64_t maxRemainingBodyLength) mutable { + /* After the JS call this lambda may be gone with the response, so nothing captured is used after it */ + auto finish = [&](std::string_view json, bool tooBig) { + HandleScope hs(isolate); + Local value; + bool parsed = !tooBig && parseJson(isolate, json).ToLocal(&value); + if (!callback.IsEmpty()) { + Local argv[] = {parsed ? value : Local(Undefined(isolate))}; + CallJS(isolate, callback.Get(isolate), 1, argv); + } else if (parsed) { + /* In a callback scope the awaiting code runs before it closes, so its writes are still corked */ + insideCorkCallback++; + { + node::CallbackScope scope(isolate, isolate->GetCurrentContext()->Global(), {0, 0}); + resolver.Get(isolate)->Resolve(isolate->GetCurrentContext(), value).FromMaybe(false); + } + insideCorkCallback--; + } else { + setInternalPointer(resObject.Get(isolate), nullptr); + res->writeStatus(tooBig ? "413 Payload Too Large" : "400 Bad Request")->end({}, true); + } + }; + + if (overflow) { + return; + } else if (!buffer) { + if (data.size() > maxSize) { + overflow = true; + finish({}, true); + } else if (maxRemainingBodyLength == 0) { + /* Single chunk: parsed straight from the received data */ + finish(data, false); + } else { + buffer = std::make_unique>(); + if (maxRemainingBodyLength <= maxSize - data.size()) { + buffer->reserve(maxRemainingBodyLength + data.size()); + } + buffer->assign(data.begin(), data.end()); + } + } else if (data.size() > maxSize - buffer->size()) { + buffer.reset(); + overflow = true; + finish({}, true); + } else { + buffer->insert(buffer->end(), data.begin(), data.end()); + if (maxRemainingBodyLength == 0) { + auto body = std::move(buffer); + finish({body->data(), body->size()}, false); + } + } + }); + } + /* Takes function of chunk and maxRemainingBodyLength. Returns this. * If maxRemainingBodyLength is 0, the last chunk has arrived. */ template @@ -613,6 +716,7 @@ struct HttpResponseWrapper { if constexpr (SSL != 2) { resTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "onDataV2", NewStringType::kNormal).ToLocalChecked(), FunctionTemplate::New(isolate, res_onDataV2)); resTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "collectBody", NewStringType::kNormal).ToLocalChecked(), FunctionTemplate::New(isolate, res_collectBody)); + resTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "collectJson", NewStringType::kNormal).ToLocalChecked(), FunctionTemplate::New(isolate, res_collectJson)); resTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "getWriteOffset", NewStringType::kNormal).ToLocalChecked(), FunctionTemplate::New(isolate, res_getWriteOffset)); resTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "beginWrite", NewStringType::kNormal).ToLocalChecked(), FunctionTemplate::New(isolate, res_beginWrite)); resTemplateLocal->PrototypeTemplate()->Set(String::NewFromUtf8(isolate, "getRemoteAddress", NewStringType::kNormal).ToLocalChecked(), FunctionTemplate::New(isolate, res_getRemoteAddress));