]> git.kaiwu.me - njs.git/commitdiff
HTTP: added r.readRequestText() and friends.
authorDmitry Volyntsev <xeioex@nginx.com>
Sat, 28 Mar 2026 01:43:56 +0000 (18:43 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Wed, 6 May 2026 00:50:59 +0000 (17:50 -0700)
Added async methods
    - r.readRequestText() as string
    - r.readRequestArrayBuffer() as ArrayBuffer
    - r.readRequestJSON() as object.

that return Promises resolving with the request body wrapped
as a corresponding type.

nginx/ngx_http_js_module.c
nginx/ngx_js.h
nginx/ngx_js_fetch.c
nginx/ngx_qjs_fetch.c
nginx/t/js_access_body.t [new file with mode: 0644]
test/ts/test.ts
ts/ngx_http_js_module.d.ts

index 1654b310bae6529a60c2a40504abeedef667300a..a448ce5796de66b542c97b1432abf7677503fc24 100644 (file)
@@ -77,6 +77,50 @@ struct ngx_http_js_ctx_s {
     ngx_js_periodic_t     *periodic;
 
     unsigned               in_progress:1;
+
+    /*
+     * Body-read ownership state for the js_access phase handler.
+     *
+     * readRequest*() cannot call ngx_http_read_client_request_body()
+     * from the JS native directly.  The phase handler must choose between
+     * NGX_AGAIN ("I still own the request") and NGX_DONE ("I finalized
+     * it myself"), but which one is correct depends on whether the body
+     * read completes synchronously or goes async -- and that is only
+     * known after the call returns.
+     *
+     * In the async case the body reader takes over r->read_event_handler
+     * and may call ngx_http_finalize_request() on I/O errors without
+     * invoking our post_handler.  If the phase handler had returned
+     * NGX_AGAIN, both the phase engine and the body reader would own
+     * the request, causing a hang on errors such as chunked 413.
+     *
+     *   IDLE         no body read requested (initial and terminal state).
+     *   DEFERRED     JS called readRequest*(); the access handler
+     *                will start the read after engine->call() returns.
+     *   IN_PROGRESS  ngx_http_read_client_request_body() returned
+     *                NGX_AGAIN; request ownership transferred to the
+     *                body reader via NGX_DONE.  access_body_done
+     *                callback resumes phases on completion.
+     *
+     * IDLE -> DEFERRED -> IDLE                  (sync completion or error)
+     * IDLE -> DEFERRED -> IN_PROGRESS -> IDLE   (async completion)
+     */
+#define NGX_HTTP_JS_BODY_READ_IDLE 0
+#define NGX_HTTP_JS_BODY_READ_DEFERRED 1
+#define NGX_HTTP_JS_BODY_READ_IN_PROGRESS 2
+    unsigned               body_read_state:2;
+
+    /*
+     * Collected request body as a contiguous buffer.
+     * Shared by both synchronous property getters (requestText,
+     * requestBuffer) and async readRequest*() methods.
+     */
+    unsigned               body_read_nul:1;
+    u_char                *body_read_data;
+    size_t                 body_read_len;
+
+    /* Pending promise/event for deferred body reads. */
+    void                  *body_read_event;
 };
 
 
@@ -129,6 +173,25 @@ static ngx_int_t ngx_http_js_variable_var(ngx_http_request_t *r,
     ngx_http_variable_value_t *v, uintptr_t data);
 static ngx_int_t ngx_http_js_init_vm(ngx_http_request_t *r, njs_int_t proto_id);
 static void ngx_http_js_cleanup_ctx(void *data);
+static void ngx_http_js_body_read_abort(ngx_http_js_ctx_t *ctx);
+static ngx_int_t ngx_http_js_collect_body(ngx_http_request_t *r,
+    ngx_http_js_ctx_t *ctx);
+static void ngx_http_js_access_body_finalize(ngx_http_request_t *r,
+    ngx_http_js_ctx_t *ctx, ngx_int_t rc);
+static void ngx_http_js_access_body_done(ngx_http_request_t *r);
+static ngx_int_t ngx_http_js_body_resolve(ngx_http_js_ctx_t *ctx,
+    void *event);
+
+static njs_int_t ngx_http_js_ext_read_request_body(njs_vm_t *vm,
+    njs_value_t *args, njs_uint_t nargs, njs_index_t magic,
+    njs_value_t *retval);
+#if (NJS_HAVE_QUICKJS)
+static JSValue ngx_http_qjs_ext_read_request_body(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv, int magic);
+static ngx_int_t ngx_http_qjs_body_resolve(ngx_http_js_ctx_t *ctx,
+    void *event);
+static void ngx_http_js_read_body_event_destructor(ngx_qjs_event_t *event);
+#endif
 
 static njs_int_t ngx_http_js_ext_keys_header(njs_vm_t *vm, njs_value_t *value,
     njs_value_t *keys, ngx_list_t *headers);
@@ -999,6 +1062,42 @@ static njs_external_t  ngx_http_js_ext_request[] = {
         }
     },
 
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("readRequestArrayBuffer"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = ngx_http_js_ext_read_request_body,
+            .magic8 = NGX_JS_BODY_ARRAY_BUFFER,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("readRequestJSON"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = ngx_http_js_ext_read_request_body,
+            .magic8 = NGX_JS_BODY_JSON,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("readRequestText"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = ngx_http_js_ext_read_request_body,
+            .magic8 = NGX_JS_BODY_TEXT,
+        }
+    },
+
     {
         .flags = NJS_EXTERN_METHOD,
         .name.string = njs_str("subrequest"),
@@ -1168,6 +1267,13 @@ static const JSCFunctionListEntry ngx_http_qjs_ext_request[] = {
     JS_CFUNC_DEF("setReturnValue", 1, ngx_http_qjs_ext_set_return_value),
     JS_CGETSET_DEF("status", ngx_http_qjs_ext_status_get,
                    ngx_http_qjs_ext_status_set),
+    JS_CFUNC_MAGIC_DEF("readRequestArrayBuffer", 0,
+                       ngx_http_qjs_ext_read_request_body,
+                       NGX_JS_BODY_ARRAY_BUFFER),
+    JS_CFUNC_MAGIC_DEF("readRequestJSON", 0,
+                       ngx_http_qjs_ext_read_request_body, NGX_JS_BODY_JSON),
+    JS_CFUNC_MAGIC_DEF("readRequestText", 0,
+                       ngx_http_qjs_ext_read_request_body, NGX_JS_BODY_TEXT),
     JS_CFUNC_DEF("subrequest", 3, ngx_http_qjs_ext_subrequest),
     JS_CGETSET_MAGIC_DEF("uri", ngx_http_qjs_ext_string, NULL,
                          offsetof(ngx_http_request_t, uri)),
@@ -1293,6 +1399,38 @@ ngx_http_js_access_handler(ngx_http_request_t *r)
         return NGX_HTTP_INTERNAL_SERVER_ERROR;
     }
 
+    /* JS called readRequest*(). */
+
+    if (ctx->body_read_state == NGX_HTTP_JS_BODY_READ_DEFERRED) {
+
+        rc = ngx_http_read_client_request_body(r, ngx_http_js_access_body_done);
+
+        if (rc >= NGX_HTTP_SPECIAL_RESPONSE) {
+            ngx_http_js_body_read_abort(ctx);
+            return rc;
+        }
+
+        r->preserve_body = 1;
+
+        if (rc == NGX_OK) {
+            /*
+             * Sync: access_body_done callback already fired, resolved
+             * or rejected the promise.  access_body_finalize() returned
+             * without running posted requests.  Fall through to let
+             * the pending/status check handle the result.
+             */
+            ctx->body_read_state = NGX_HTTP_JS_BODY_READ_IDLE;
+            goto done;
+        }
+
+        ctx->body_read_state = NGX_HTTP_JS_BODY_READ_IN_PROGRESS;
+        ctx->in_progress = 1;
+        ngx_http_finalize_request(r, NGX_DONE);
+        return NGX_DONE;
+    }
+
+done:
+
     if (ngx_js_ctx_pending(ctx)) {
         ctx->in_progress = 1;
         r->write_event_handler = ngx_http_js_access_write_event_handler;
@@ -3129,14 +3267,9 @@ ngx_http_js_ext_get_request_body(njs_vm_t *vm, njs_object_prop_t *prop,
     uint32_t unused, njs_value_t *value, njs_value_t *setval,
     njs_value_t *retval)
 {
-    u_char              *p, *body;
-    size_t               len;
-    ssize_t              n;
     uint32_t             buffer_type;
-    ngx_buf_t           *buf;
     njs_int_t            ret;
     njs_value_t         *request_body;
-    ngx_chain_t         *cl;
     ngx_http_js_ctx_t   *ctx;
     ngx_http_request_t  *r;
 
@@ -3164,6 +3297,43 @@ ngx_http_js_ext_get_request_body(njs_vm_t *vm, njs_object_prop_t *prop,
         return NJS_DECLINED;
     }
 
+    if (ngx_http_js_collect_body(r, ctx) != NGX_OK) {
+        njs_vm_internal_error(vm, "failed to read request body");
+        return NJS_ERROR;
+    }
+
+    ret = ngx_js_prop(vm, buffer_type, request_body, ctx->body_read_data,
+                      ctx->body_read_len);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    njs_value_assign(retval, request_body);
+
+    return NJS_OK;
+}
+
+
+static ngx_int_t
+ngx_http_js_collect_body(ngx_http_request_t *r, ngx_http_js_ctx_t *ctx)
+{
+    u_char       *p, *body;
+    size_t        len;
+    ssize_t       n;
+    ngx_buf_t    *buf;
+    ngx_chain_t  *cl;
+
+    if (ctx->body_read_data != NULL) {
+        return NGX_OK;
+    }
+
+    if (r->request_body == NULL || r->request_body->bufs == NULL) {
+        ctx->body_read_data = (u_char *) "";
+        ctx->body_read_len = 0;
+        ctx->body_read_nul = 1;
+        return NGX_OK;
+    }
+
     cl = r->request_body->bufs;
     buf = cl->buf;
 
@@ -3172,66 +3342,289 @@ ngx_http_js_ext_get_request_body(njs_vm_t *vm, njs_object_prop_t *prop,
                       "http js reading request body from a temporary file");
 
         if (buf == NULL || !buf->in_file) {
-            njs_vm_internal_error(vm, "cannot find request body");
-            return NJS_ERROR;
+            return NGX_ERROR;
         }
 
         len = buf->file_last - buf->file_pos;
 
-        body = ngx_pnalloc(r->pool, len);
+        body = ngx_pnalloc(r->pool, len + 1);
         if (body == NULL) {
-            njs_vm_memory_error(vm);
-            return NJS_ERROR;
+            return NGX_ERROR;
         }
 
         n = ngx_read_file(buf->file, body, len, buf->file_pos);
         if (n != (ssize_t) len) {
-            njs_vm_internal_error(vm, "failed to read request body");
-            return NJS_ERROR;
+            return NGX_ERROR;
         }
 
-        goto done;
-    }
+        body[len] = '\0';
+        ctx->body_read_nul = 1;
 
-    if (cl->next == NULL) {
+    } else if (cl->next == NULL) {
         len = buf->last - buf->pos;
         body = buf->pos;
 
-        goto done;
+    } else {
+        len = buf->last - buf->pos;
+        cl = cl->next;
+
+        for ( /* void */ ; cl; cl = cl->next) {
+            buf = cl->buf;
+            len += buf->last - buf->pos;
+        }
+
+        p = ngx_pnalloc(r->pool, len + 1);
+        if (p == NULL) {
+            return NGX_ERROR;
+        }
+
+        body = p;
+        cl = r->request_body->bufs;
+
+        for ( /* void */ ; cl; cl = cl->next) {
+            buf = cl->buf;
+            p = ngx_cpymem(p, buf->pos, buf->last - buf->pos);
+        }
+
+        *p = '\0';
+        ctx->body_read_nul = 1;
     }
 
-    len = buf->last - buf->pos;
-    cl = cl->next;
+    ctx->body_read_data = body;
+    ctx->body_read_len = len;
+
+    return NGX_OK;
+}
+
 
-    for ( /* void */ ; cl; cl = cl->next) {
-        buf = cl->buf;
-        len += buf->last - buf->pos;
+static void
+ngx_http_js_body_read_abort(ngx_http_js_ctx_t *ctx)
+{
+    if (ctx->body_read_event != NULL) {
+#if (NJS_HAVE_QUICKJS)
+        if (ctx->engine->type == NGX_ENGINE_QJS) {
+            ngx_js_del_event(ctx, (ngx_qjs_event_t *) ctx->body_read_event);
+        } else
+#endif
+        {
+            ngx_js_del_event(ctx, (ngx_js_event_t *) ctx->body_read_event);
+        }
+
+        ctx->body_read_event = NULL;
     }
 
-    p = ngx_pnalloc(r->pool, len);
-    if (p == NULL) {
+    ctx->body_read_state = NGX_HTTP_JS_BODY_READ_IDLE;
+}
+
+
+static njs_int_t
+ngx_http_js_body_to_value(njs_vm_t *vm, ngx_http_js_ctx_t *ctx,
+    ngx_uint_t type, njs_value_t *retval)
+{
+    njs_int_t           ret;
+    njs_opaque_value_t  arg;
+
+    switch (type) {
+    case NGX_JS_BODY_ARRAY_BUFFER:
+        return njs_vm_value_array_buffer_set(vm, retval,
+                                             ctx->body_read_data,
+                                             ctx->body_read_len);
+
+    case NGX_JS_BODY_JSON:
+        ret = njs_vm_value_string_create(vm, njs_value_arg(&arg),
+                                         ctx->body_read_data,
+                                         ctx->body_read_len);
+        if (ret != NJS_OK) {
+            return NJS_ERROR;
+        }
+
+        return njs_vm_json_parse(vm, njs_value_arg(&arg), 1, retval);
+
+    case NGX_JS_BODY_TEXT:
+    default:
+        return njs_vm_value_string_create(vm, retval,
+                                          ctx->body_read_data,
+                                          ctx->body_read_len);
+    }
+}
+
+
+static void
+ngx_http_js_access_body_finalize(ngx_http_request_t *r, ngx_http_js_ctx_t *ctx,
+    ngx_int_t rc)
+{
+    switch (ctx->body_read_state) {
+    case NGX_HTTP_JS_BODY_READ_IN_PROGRESS:
+        ctx->body_read_state = NGX_HTTP_JS_BODY_READ_IDLE;
+
+        if (ngx_js_ctx_pending(ctx)) {
+            r->write_event_handler = ngx_http_js_access_write_event_handler;
+            return;
+        }
+
+        r->write_event_handler = ngx_http_core_run_phases;
+        ngx_http_core_run_phases(r);
+        break;
+
+    case NGX_HTTP_JS_BODY_READ_DEFERRED:
+        /*
+         * Sync body read completion from the access handler.
+         * The promise is resolved/rejected but the access handler
+         * is still on the call stack -- do not run posted requests
+         * or resume phases here; the access handler will do it.
+         */
+        break;
+
+    case NGX_HTTP_JS_BODY_READ_IDLE:
+    default:
+        ngx_http_js_event_finalize(r, rc);
+    }
+}
+
+
+static ngx_int_t
+ngx_http_js_body_resolve(ngx_http_js_ctx_t *ctx, void *event)
+{
+    njs_vm_t            *vm;
+    njs_int_t            rc;
+    ngx_js_event_t      *ev;
+    njs_opaque_value_t   result;
+
+    ev = event;
+    vm = ctx->engine->u.njs.vm;
+
+    rc = ngx_http_js_body_to_value(vm, ctx, (uintptr_t) ev->data,
+                                   njs_value_arg(&result));
+    if (rc != NJS_OK) {
+        njs_vm_exception_get(vm, njs_value_arg(&result));
+
+        rc = ngx_js_call(vm, njs_value_function(njs_value_arg(&ev->args[1])),
+                         &result, 1);
+
+        ngx_js_del_event(ctx, ev);
+        return rc;
+    }
+
+    rc = ngx_js_call(vm, njs_value_function(njs_value_arg(&ev->function)),
+                     &result, 1);
+
+    ngx_js_del_event(ctx, ev);
+
+    return rc;
+}
+
+
+static void
+ngx_http_js_access_body_done(ngx_http_request_t *r)
+{
+    ngx_int_t           rc;
+    ngx_http_js_ctx_t  *ctx;
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                   "http js body read done");
+
+    /*
+     * ngx_http_read_client_request_body() incremented count.
+     * For the IN_PROGRESS (async) path, ngx_http_finalize_request(NGX_DONE)
+     * already consumed it; for the DEFERRED (sync) path, we consume it here.
+     */
+    if (ctx->body_read_state == NGX_HTTP_JS_BODY_READ_DEFERRED) {
+        r->main->count--;
+    }
+
+    if (ctx->body_read_event == NULL) {
+        return;
+    }
+
+    if (ngx_http_js_collect_body(r, ctx) != NGX_OK) {
+        ngx_http_js_body_read_abort(ctx);
+        ngx_http_js_access_body_finalize(r, ctx, NGX_ERROR);
+        return;
+    }
+
+#if (NJS_HAVE_QUICKJS)
+    if (ctx->engine->type == NGX_ENGINE_QJS) {
+        rc = ngx_http_qjs_body_resolve(ctx, ctx->body_read_event);
+    } else
+#endif
+    {
+        rc = ngx_http_js_body_resolve(ctx, ctx->body_read_event);
+    }
+
+    ctx->body_read_event = NULL;
+
+    ngx_http_js_access_body_finalize(r, ctx, rc);
+}
+
+
+static njs_int_t
+ngx_http_js_ext_read_request_body(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t magic, njs_value_t *retval)
+{
+    ngx_int_t            rc;
+    ngx_js_event_t      *event;
+    ngx_http_js_ctx_t   *ctx;
+    ngx_http_request_t  *r;
+
+    r = njs_vm_external(vm, ngx_http_js_request_proto_id,
+                        njs_argument(args, 0));
+    if (r == NULL) {
+        njs_vm_error(vm, "\"this\" is not an external");
+        return NJS_ERROR;
+    }
+
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    event = njs_mp_zalloc(njs_vm_memory_pool(vm),
+                          sizeof(ngx_js_event_t)
+                          + sizeof(njs_opaque_value_t) * 2);
+    if (njs_slow_path(event == NULL)) {
         njs_vm_memory_error(vm);
         return NJS_ERROR;
     }
 
-    body = p;
-    cl = r->request_body->bufs;
+    /*
+     * r->request_body is set by ngx_http_read_client_request_body().
+     * JS only runs after body reading completes, so non-NULL means
+     * the body is available.
+     */
+    if (r->request_body) {
+        goto resolve;
+    }
 
-    for ( /* void */ ; cl; cl = cl->next) {
-        buf = cl->buf;
-        p = ngx_cpymem(p, buf->pos, buf->last - buf->pos);
+    if (ctx->body_read_event) {
+        njs_vm_error(vm, "request body is already being read");
+        return NJS_ERROR;
     }
 
-done:
+    event->fd = ctx->event_id++;
+    event->args = (njs_opaque_value_t *) &event[1];
+    event->data = (void *) (uintptr_t) magic;
 
-    ret = ngx_js_prop(vm, buffer_type, request_body, body, len);
-    if (ret != NJS_OK) {
+    rc = njs_vm_promise_create(vm, retval, njs_value_arg(event->args));
+    if (rc != NJS_OK) {
         return NJS_ERROR;
     }
 
-    njs_value_assign(retval, request_body);
+    njs_value_assign(&event->function, njs_value_arg(event->args));
+
+    ngx_js_add_event(ctx, event);
+
+    ctx->body_read_event = event;
+    ctx->body_read_state = NGX_HTTP_JS_BODY_READ_DEFERRED;
 
     return NJS_OK;
+
+resolve:
+
+    if (ngx_http_js_collect_body(r, ctx) != NGX_OK) {
+        njs_vm_memory_error(vm);
+        return NJS_ERROR;
+    }
+
+    return ngx_http_js_body_to_value(vm, ctx, (ngx_uint_t) magic, retval);
 }
 
 
@@ -5486,14 +5879,10 @@ ngx_http_qjs_ext_response_body(JSContext *cx, JSValueConst this_val, int type)
 static JSValue
 ngx_http_qjs_ext_request_body(JSContext *cx, JSValueConst this_val, int type)
 {
-    u_char                  *p, *data;
-    size_t                   len;
-    ssize_t                  n;
     JSValue                  body;
     uint32_t                 buffer_type;
-    ngx_buf_t               *buf;
-    ngx_chain_t             *cl;
     ngx_http_request_t      *r;
+    ngx_http_js_ctx_t       *ctx;
     ngx_http_qjs_request_t  *req;
 
     req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
@@ -5517,70 +5906,170 @@ ngx_http_qjs_ext_request_body(JSContext *cx, JSValueConst this_val, int type)
         return JS_UNDEFINED;
     }
 
-    cl = r->request_body->bufs;
-    buf = cl->buf;
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
 
-    if (r->request_body->temp_file) {
-        ngx_log_error(NGX_LOG_WARN, r->connection->log, 0,
-                      "http js reading request body from a temporary file");
+    if (ngx_http_js_collect_body(r, ctx) != NGX_OK) {
+        return JS_ThrowInternalError(cx, "failed to read request body");
+    }
 
-        if (buf == NULL || !buf->in_file) {
-            return JS_ThrowInternalError(cx, "cannot find body file");
-        }
+    body = ngx_qjs_prop(cx, buffer_type, ctx->body_read_data,
+                        ctx->body_read_len);
+    if (JS_IsException(body)) {
+        return JS_EXCEPTION;
+    }
 
-        len = buf->file_last - buf->file_pos;
+    req->request_body = body;
 
-        data = ngx_pnalloc(r->pool, len);
-        if (data == NULL) {
-            return JS_ThrowOutOfMemory(cx);
+    return JS_DupValue(cx, req->request_body);
+}
+
+
+static JSValue
+ngx_http_qjs_body_to_value(JSContext *cx, ngx_http_js_ctx_t *ctx,
+    ngx_uint_t type)
+{
+    JSValue      str;
+    const char  *cstr;
+
+    switch (type) {
+    case NGX_JS_BODY_ARRAY_BUFFER:
+        return JS_NewArrayBuffer(cx, ctx->body_read_data,
+                                 ctx->body_read_len, NULL, NULL, 0);
+
+    case NGX_JS_BODY_JSON:
+        if (ctx->body_read_nul) {
+            return JS_ParseJSON(cx, (const char *) ctx->body_read_data,
+                                ctx->body_read_len, "<body>");
         }
 
-        n = ngx_read_file(buf->file, data, len, buf->file_pos);
-        if (n != (ssize_t) len) {
-            return JS_ThrowInternalError(cx, "failed to read request body");
+        str = qjs_string_create(cx, ctx->body_read_data,
+                                ctx->body_read_len);
+        if (JS_IsException(str)) {
+            return str;
+        }
+
+        cstr = JS_ToCString(cx, str);
+        JS_FreeValue(cx, str);
+
+        if (cstr == NULL) {
+            return JS_EXCEPTION;
         }
 
-        goto done;
+        str = JS_ParseJSON(cx, cstr, ctx->body_read_len, "<body>");
+        JS_FreeCString(cx, cstr);
+
+        return str;
+
+    case NGX_JS_BODY_TEXT:
+    default:
+        return qjs_string_create(cx, ctx->body_read_data,
+                                 ctx->body_read_len);
     }
+}
+
+
+static ngx_int_t
+ngx_http_qjs_body_resolve(ngx_http_js_ctx_t *ctx, void *event)
+{
+    JSValue           result;
+    JSContext        *cx;
+    ngx_int_t         rc;
+    ngx_qjs_event_t  *ev;
+
+    ev = event;
+    cx = ctx->engine->u.qjs.ctx;
+
+    result = ngx_http_qjs_body_to_value(cx, ctx, (uintptr_t) ev->data);
+    if (JS_IsException(result)) {
+        result = JS_GetException(cx);
+
+        rc = ngx_qjs_call(cx, ev->args[1], &result, 1);
+
+        JS_FreeValue(cx, result);
+        ngx_js_del_event(ctx, ev);
+        return rc;
+    }
+
+    rc = ngx_qjs_call(cx, ev->function, &result, 1);
+
+    JS_FreeValue(cx, result);
+    ngx_js_del_event(ctx, ev);
+
+    return rc;
+}
 
-    if (cl->next == NULL) {
-        len = buf->last - buf->pos;
-        data = buf->pos;
 
-        goto done;
+static JSValue
+ngx_http_qjs_ext_read_request_body(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv, int magic)
+{
+    JSValue                  retval;
+    ngx_qjs_event_t         *event;
+    ngx_http_js_ctx_t       *ctx;
+    ngx_http_request_t      *r;
+    ngx_http_qjs_request_t  *req;
+
+    req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
+    if (req == NULL) {
+        return JS_ThrowInternalError(cx, "\"this\" is not a request object");
     }
 
-    len = buf->last - buf->pos;
-    cl = cl->next;
+    r = req->request;
+    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+
+    if (r->request_body) {
+        goto resolve;
+    }
 
-    for ( /* void */ ; cl; cl = cl->next) {
-        buf = cl->buf;
-        len += buf->last - buf->pos;
+    if (ctx->body_read_event) {
+        return JS_ThrowInternalError(cx, "request body is already being read");
     }
 
-    p = ngx_pnalloc(r->pool, len);
-    if (p == NULL) {
+    event = ngx_pcalloc(r->pool, sizeof(ngx_qjs_event_t) + sizeof(JSValue) * 2);
+    if (event == NULL) {
         return JS_ThrowOutOfMemory(cx);
     }
 
-    data = p;
-    cl = r->request_body->bufs;
+    event->ctx = cx;
+    event->fd = ctx->event_id++;
+    event->args = (JSValue *) &event[1];
+    event->data = (void *) (uintptr_t) magic;
 
-    for ( /* void */ ; cl; cl = cl->next) {
-        buf = cl->buf;
-        p = ngx_cpymem(p, buf->pos, buf->last - buf->pos);
+    retval = JS_NewPromiseCapability(cx, &event->args[0]);
+    if (JS_IsException(retval)) {
+        return JS_EXCEPTION;
     }
 
-done:
+    event->function = JS_DupValue(cx, event->args[0]);
+    event->destructor = ngx_http_js_read_body_event_destructor;
 
-    body = ngx_qjs_prop(cx, buffer_type, data, len);
-    if (JS_IsException(body)) {
-        return JS_EXCEPTION;
+    ngx_js_add_event(ctx, event);
+
+    ctx->body_read_event = event;
+    ctx->body_read_state = NGX_HTTP_JS_BODY_READ_DEFERRED;
+
+    return retval;
+
+resolve:
+
+    if (ngx_http_js_collect_body(r, ctx) != NGX_OK) {
+        return JS_ThrowOutOfMemory(cx);
     }
 
-    req->request_body = body;
+    return ngx_http_qjs_body_to_value(cx, ctx, (ngx_uint_t) magic);
+}
 
-    return JS_DupValue(cx, req->request_body);
+
+static void
+ngx_http_js_read_body_event_destructor(ngx_qjs_event_t *event)
+{
+    JSContext  *cx;
+
+    cx = event->ctx;
+
+    JS_FreeValue(cx, event->function);
+    JS_FreeValue(cx, event->args[0]);
+    JS_FreeValue(cx, event->args[1]);
 }
 
 
index 6012e7fd9489c1545dd145109243f5a668b937ec..63cbf00a6a770155c6981200809d696115f0e24d 100644 (file)
 #define NGX_JS_BOOLEAN      8
 #define NGX_JS_NUMBER       16
 
+#define NGX_JS_BODY_ARRAY_BUFFER   0
+#define NGX_JS_BODY_JSON           1
+#define NGX_JS_BODY_TEXT           2
+
 #define NGX_JS_BOOL_FALSE   0
 #define NGX_JS_BOOL_TRUE    1
 #define NGX_JS_BOOL_UNSET   2
index fd40a5bad38a746ed8e77e4ab357483b806277d7..608af25ba37c041c5f7576ca9efcbf681484e59f 100644 (file)
@@ -268,9 +268,6 @@ static njs_external_t  ngx_js_ext_http_request[] = {
         .enumerable = 1,
         .u.method = {
             .native = ngx_request_js_ext_body,
-#define NGX_JS_BODY_ARRAY_BUFFER   0
-#define NGX_JS_BODY_JSON           1
-#define NGX_JS_BODY_TEXT           2
             .magic8 = NGX_JS_BODY_ARRAY_BUFFER
         }
     },
index 8e010bec824e2dd7d2e943c3b476b3aa0ba3bca1..7c0a7a34d09a3329b0910c9c1df508dd6d3c7378 100644 (file)
@@ -131,22 +131,19 @@ static const JSCFunctionListEntry  ngx_qjs_ext_fetch_headers_proto[] = {
 
 
 static const JSCFunctionListEntry  ngx_qjs_ext_fetch_request_proto[] = {
-#define NGX_QJS_BODY_ARRAY_BUFFER   0
-#define NGX_QJS_BODY_JSON           1
-#define NGX_QJS_BODY_TEXT           2
     JS_CFUNC_MAGIC_DEF("arrayBuffer", 0, ngx_qjs_ext_fetch_request_body,
-                       NGX_QJS_BODY_ARRAY_BUFFER),
+                       NGX_JS_BODY_ARRAY_BUFFER),
     JS_CGETSET_DEF("bodyUsed", ngx_qjs_ext_fetch_request_body_used, NULL),
     JS_CGETSET_DEF("cache", ngx_qjs_ext_fetch_request_cache, NULL),
     JS_CGETSET_DEF("credentials", ngx_qjs_ext_fetch_request_credentials, NULL),
     JS_CFUNC_MAGIC_DEF("json", 0, ngx_qjs_ext_fetch_request_body,
-                       NGX_QJS_BODY_JSON),
+                       NGX_JS_BODY_JSON),
     JS_CGETSET_DEF("headers", ngx_qjs_ext_fetch_request_headers, NULL ),
     JS_CGETSET_MAGIC_DEF("method", ngx_qjs_ext_fetch_request_field, NULL,
                          offsetof(ngx_js_request_t, method) ),
     JS_CGETSET_DEF("mode", ngx_qjs_ext_fetch_request_mode, NULL),
     JS_CFUNC_MAGIC_DEF("text", 0, ngx_qjs_ext_fetch_request_body,
-                       NGX_QJS_BODY_TEXT),
+                       NGX_JS_BODY_TEXT),
     JS_CGETSET_MAGIC_DEF("url", ngx_qjs_ext_fetch_request_field, NULL,
                          offsetof(ngx_js_request_t, url) ),
 };
@@ -154,17 +151,17 @@ static const JSCFunctionListEntry  ngx_qjs_ext_fetch_request_proto[] = {
 
 static const JSCFunctionListEntry  ngx_qjs_ext_fetch_response_proto[] = {
     JS_CFUNC_MAGIC_DEF("arrayBuffer", 0, ngx_qjs_ext_fetch_response_body,
-                       NGX_QJS_BODY_ARRAY_BUFFER),
+                       NGX_JS_BODY_ARRAY_BUFFER),
     JS_CGETSET_DEF("bodyUsed", ngx_qjs_ext_fetch_response_body_used, NULL),
     JS_CGETSET_DEF("headers", ngx_qjs_ext_fetch_response_headers, NULL ),
     JS_CFUNC_MAGIC_DEF("json", 0, ngx_qjs_ext_fetch_response_body,
-                       NGX_QJS_BODY_JSON),
+                       NGX_JS_BODY_JSON),
     JS_CGETSET_DEF("ok", ngx_qjs_ext_fetch_response_ok, NULL),
     JS_CGETSET_DEF("redirected", ngx_qjs_ext_fetch_response_redirected, NULL),
     JS_CGETSET_DEF("status", ngx_qjs_ext_fetch_response_status, NULL),
     JS_CGETSET_DEF("statusText", ngx_qjs_ext_fetch_response_status_text, NULL),
     JS_CFUNC_MAGIC_DEF("text", 0, ngx_qjs_ext_fetch_response_body,
-                       NGX_QJS_BODY_TEXT),
+                       NGX_JS_BODY_TEXT),
     JS_CGETSET_DEF("type", ngx_qjs_ext_fetch_response_type, NULL),
     JS_CGETSET_MAGIC_DEF("url", ngx_qjs_ext_fetch_response_field, NULL,
                          offsetof(ngx_js_response_t, url) ),
@@ -2027,7 +2024,7 @@ ngx_qjs_ext_fetch_request_body(JSContext *cx, JSValueConst this_val,
     request->body_used = 1;
 
     switch (magic) {
-    case NGX_QJS_BODY_ARRAY_BUFFER:
+    case NGX_JS_BODY_ARRAY_BUFFER:
         /*
          * no free_func for JS_NewArrayBuffer()
          * because request->body is allocated from e->pool
@@ -2041,15 +2038,15 @@ ngx_qjs_ext_fetch_request_body(JSContext *cx, JSValueConst this_val,
 
         break;
 
-    case NGX_QJS_BODY_JSON:
-    case NGX_QJS_BODY_TEXT:
+    case NGX_JS_BODY_JSON:
+    case NGX_JS_BODY_TEXT:
     default:
         result = qjs_string_create(cx, request->body.data, request->body.len);
         if (JS_IsException(result)) {
             return JS_ThrowOutOfMemory(cx);
         }
 
-        if (magic == NGX_QJS_BODY_JSON) {
+        if (magic == NGX_JS_BODY_JSON) {
             string = js_malloc(cx, request->body.len + 1);
 
             JS_FreeValue(cx, result);
@@ -2309,14 +2306,14 @@ ngx_qjs_ext_fetch_response_body(JSContext *cx, JSValueConst this_val,
     response->body_used = 1;
 
     switch (magic) {
-    case NGX_QJS_BODY_ARRAY_BUFFER:
-    case NGX_QJS_BODY_TEXT:
+    case NGX_JS_BODY_ARRAY_BUFFER:
+    case NGX_JS_BODY_TEXT:
         ret = njs_chb_join(&response->chain, &string);
         if (ret != NJS_OK) {
             return JS_ThrowOutOfMemory(cx);
         }
 
-        if (magic == NGX_QJS_BODY_TEXT) {
+        if (magic == NGX_JS_BODY_TEXT) {
             result = qjs_string_create(cx, string.start, string.length);
             if (JS_IsException(result)) {
                 return JS_ThrowOutOfMemory(cx);
@@ -2338,7 +2335,7 @@ ngx_qjs_ext_fetch_response_body(JSContext *cx, JSValueConst this_val,
 
         break;
 
-    case NGX_QJS_BODY_JSON:
+    case NGX_JS_BODY_JSON:
     default:
         /* 'string.start' must be zero terminated. */
         njs_chb_append_literal(&response->chain, "\0");
diff --git a/nginx/t/js_access_body.t b/nginx/t/js_access_body.t
new file mode 100644 (file)
index 0000000..abe53b3
--- /dev/null
@@ -0,0 +1,459 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) F5, Inc.
+
+# Tests for http njs module, js_access body reading methods.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+use Socket qw/ CRLF /;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx qw/ :DEFAULT http_end /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http proxy/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    js_import test.js;
+
+    js_var $foo;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /text {
+            js_access test.read_text;
+            js_content test.content;
+        }
+
+        location /buffer {
+            js_access test.read_buffer;
+            js_content test.content;
+        }
+
+        location /text_twice {
+            js_access test.read_text_twice;
+            js_content test.content;
+        }
+
+        location /buffer_twice {
+            js_access test.read_buffer_twice;
+            js_content test.content;
+        }
+
+        location /concurrent_text_buffer {
+            js_access test.read_concurrent_text_buffer;
+            js_content test.content;
+        }
+
+        location /text_then_buffer {
+            js_access test.read_text_then_buffer;
+            js_content test.content;
+        }
+
+        location /json {
+            js_access test.read_json;
+            js_content test.content;
+        }
+
+        location /json_invalid {
+            js_access test.read_json_invalid;
+            js_content test.content;
+        }
+
+        location /empty {
+            js_access test.read_text;
+            js_content test.content;
+        }
+
+        location /big {
+            client_body_buffer_size 64k;
+            js_access test.read_text_length;
+            js_content test.content;
+        }
+
+        location /big_4k {
+            client_body_buffer_size 4k;
+            js_access test.read_text_length;
+            js_content test.content;
+        }
+
+        location /slow {
+            js_access test.read_text;
+            js_content test.content;
+        }
+
+        location /chunked {
+            js_access test.read_text;
+            js_content test.content;
+        }
+
+        location /text_timeout {
+            js_access test.read_text_timeout;
+            js_content test.content;
+        }
+
+        location /access_content_async {
+            js_access test.read_text_timeout;
+            js_content test.content_async;
+        }
+
+        location /content_text {
+            js_content test.content_text;
+        }
+
+        location /proxy {
+            js_access test.read_text;
+            proxy_pass http://127.0.0.1:%%PORT_8081%%;
+        }
+
+        location /in_file {
+            client_body_in_file_only on;
+            js_access test.read_text;
+            js_content test.content;
+        }
+
+        location /too_large {
+            client_max_body_size 4;
+            js_access test.read_text;
+            js_content test.content;
+        }
+
+        location /too_large_chunked {
+            client_max_body_size 4;
+            client_body_timeout 2s;
+            js_access test.read_text;
+            js_content test.content;
+        }
+
+
+    }
+
+    server {
+        listen       127.0.0.1:8081;
+
+        location / {
+            js_content test.echo_body;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    function content(r) {
+        r.return(200, `var:\${r.variables.foo}`);
+    }
+
+    async function content_async(r) {
+        await new Promise(resolve => setTimeout(resolve, 5));
+        r.return(200, `var:\${r.variables.foo}:content-async`);
+    }
+
+    async function content_text(r) {
+        let body = await r.readRequestText();
+        r.return(200, `content:\${body}`);
+    }
+
+    async function read_text(r) {
+        let body = await r.readRequestText();
+        r.variables.foo = body;
+    }
+
+    async function read_text_timeout(r) {
+        let body = await r.readRequestText();
+        await new Promise(resolve => setTimeout(resolve, 5));
+        r.variables.foo = body + ':after-timeout';
+    }
+
+    async function read_buffer(r) {
+        let buf = await r.readRequestArrayBuffer();
+        r.variables.foo = String.fromCharCode.apply(null, new Uint8Array(buf));
+    }
+
+    async function read_text_twice(r) {
+        let first = await r.readRequestText();
+        let second = await r.readRequestText();
+        r.variables.foo = (first === second) ? 'same' : 'different';
+    }
+
+    async function read_buffer_twice(r) {
+        let a = new Uint8Array(await r.readRequestArrayBuffer());
+        let b = new Uint8Array(await r.readRequestArrayBuffer());
+        let eq = a.length === b.length
+                 && a.every((v, i) => v === b[i]);
+        r.variables.foo = eq ? 'same' : 'different';
+    }
+
+    async function read_concurrent_text_buffer(r) {
+        try {
+            await Promise.all([
+                r.readRequestText(),
+                r.readRequestArrayBuffer()
+            ]);
+
+            r.variables.foo = 'no_error';
+
+        } catch (e) {
+            r.variables.foo = e.message;
+        }
+    }
+
+    async function read_text_then_buffer(r) {
+        let text = await r.readRequestText();
+        let buf = await r.readRequestArrayBuffer();
+        let text2 = String.fromCharCode.apply(null, new Uint8Array(buf));
+        r.variables.foo = (text === text2) ? 'same' : 'different';
+    }
+
+    async function read_json(r) {
+        let obj = await r.readRequestJSON();
+        r.variables.foo = obj.method + ':' + obj.name;
+    }
+
+    async function read_json_invalid(r) {
+        try {
+            await r.readRequestJSON();
+            r.variables.foo = 'no_error';
+        } catch (e) {
+            r.variables.foo = e.constructor.name;
+        }
+    }
+
+    async function read_text_length(r) {
+        let body = await r.readRequestText();
+        r.variables.foo = body.length;
+    }
+
+    function echo_body(r) {
+        r.return(200, 'echo:' + r.requestText);
+    }
+
+    export default { content, content_async, content_text, read_text,
+                     read_text_timeout, read_buffer, read_text_twice,
+                     read_buffer_twice, read_concurrent_text_buffer,
+                     read_text_then_buffer, read_json, read_json_invalid,
+                     read_text_length, echo_body };
+
+EOF
+
+$t->try_run('no js_access')->plan(23);
+
+###############################################################################
+
+like(http_post('/text'), qr/var:REQ-BODY/, 'readRequestText');
+like(http_post('/buffer'), qr/var:REQ-BODY/, 'readRequestArrayBuffer');
+like(http_post_json('/json', '{"method":"GET","name":"test"}'),
+       qr/var:GET:test/, 'readRequestJSON');
+like(http_post_json('/json_invalid', 'not-json'), qr/var:SyntaxError/,
+       'readRequestJSON invalid rejects with SyntaxError');
+like(http_get('/empty'), qr/var:/, 'readRequestText empty body');
+
+like(http_post('/text_twice'), qr/var:same/,
+       'readRequestText twice returns same value');
+like(http_post('/buffer_twice'), qr/var:same/,
+       'readRequestArrayBuffer twice returns same value');
+like(http_post('/text_then_buffer'), qr/var:same/,
+       'readRequestText then readRequestArrayBuffer same content');
+like(http_post('/concurrent_text_buffer'),
+       qr/var:request body is already being read/,
+       'concurrent body read throws error');
+
+like(http_post_big('/big'), qr/var:10240/,
+       'readRequestText large body');
+like(http_post_big('/big_4k'), qr/var:10240/,
+       'readRequestText large body with small buffer');
+
+like(http_post('/proxy'), qr/echo:REQ-BODY/,
+       'body preserved for proxy_pass');
+like(http_post('/in_file'), qr/var:REQ-BODY/,
+       'readRequestText from temp file');
+
+like(http_post_slow('/slow'), qr/var:SLOW-BODY/,
+       'readRequestText with slow client');
+like(http_post_chunked('/chunked'), qr/var:CHUNKED-BODY/,
+       'readRequestText chunked transfer encoding');
+like(http_post('/text_timeout'), qr/var:REQ-BODY:after-timeout/,
+       'readRequestText before async action in js_access');
+like(http_post('/access_content_async'),
+       qr/var:REQ-BODY:after-timeout:content-async/,
+       'async js_content after async js_access body read');
+like(http_post('/content_text'), qr/content:REQ-BODY/,
+       'readRequestText in js_content');
+
+http_post_disconnect('/text');
+like(http_post('/text'), qr/var:REQ-BODY/,
+       'readRequestText after client disconnect');
+
+like(http_post('/too_large'), qr/413 Request Entity Too Large/,
+       'readRequestText client_max_body_size exceeded');
+
+like(http_post_slow_chunked('/too_large_chunked'),
+       qr/413 Request Entity Too Large/,
+       'readRequestText chunked body exceeds client_max_body_size');
+like(http_post_chunked_too_large('/too_large_chunked'),
+       qr/413 Request Entity Too Large/,
+       'readRequestText chunked body rejected in preread');
+like(http_post('/text'), qr/var:REQ-BODY/,
+       'readRequestText works after chunked 413');
+
+###############################################################################
+
+sub http_post {
+       my ($url, %extra) = @_;
+
+       my $p = "POST $url HTTP/1.0" . CRLF .
+               "Host: localhost" . CRLF .
+               "Content-Length: 8" . CRLF .
+               CRLF .
+               "REQ-BODY";
+
+       return http($p, %extra);
+}
+
+sub http_post_json {
+       my ($url, $body, %extra) = @_;
+
+       my $p = "POST $url HTTP/1.0" . CRLF .
+               "Host: localhost" . CRLF .
+               "Content-Type: application/json" . CRLF .
+               "Content-Length: " . length($body) . CRLF .
+               CRLF .
+               $body;
+
+       return http($p, %extra);
+}
+
+sub http_post_big {
+       my ($url, %extra) = @_;
+
+       my $p = "POST $url HTTP/1.0" . CRLF .
+               "Host: localhost" . CRLF .
+               "Content-Length: 10240" . CRLF .
+               CRLF .
+               ("1234567890" x 1024);
+
+       return http($p, %extra);
+}
+
+sub http_post_slow {
+       my ($url, %extra) = @_;
+
+       my $header = "POST $url HTTP/1.1" . CRLF .
+               "Host: localhost" . CRLF .
+               "Content-Length: 9" . CRLF .
+               "Connection: close" . CRLF .
+               CRLF;
+
+       my $s = http($header, start => 1);
+
+       select undef, undef, undef, 0.1;
+       print $s "SLOW";
+
+       select undef, undef, undef, 0.1;
+       print $s "-BODY";
+
+       return http_end($s);
+}
+
+sub http_post_disconnect {
+       my ($url) = @_;
+
+       my $header = "POST $url HTTP/1.1" . CRLF .
+               "Host: localhost" . CRLF .
+               "Content-Length: 1024" . CRLF .
+               "Connection: close" . CRLF .
+               CRLF;
+
+       my $s = http($header, start => 1);
+
+       select undef, undef, undef, 0.1;
+       print $s "PARTIAL";
+
+       select undef, undef, undef, 0.1;
+       close($s);
+
+       select undef, undef, undef, 0.3;
+}
+
+sub http_post_slow_chunked {
+       my ($url, %extra) = @_;
+
+       my $header = "POST $url HTTP/1.1" . CRLF .
+               "Host: localhost" . CRLF .
+               "Transfer-Encoding: chunked" . CRLF .
+               "Connection: close" . CRLF .
+               CRLF;
+
+       my $s = http($header, start => 1);
+
+       select undef, undef, undef, 0.1;
+       print $s "8" . CRLF . "TOOLARGE" . CRLF;
+
+       my $resp = http_end($s);
+
+       # wait for nginx to finish lingering close and cleanup
+       select undef, undef, undef, 0.5;
+
+       return $resp;
+}
+
+sub http_post_chunked {
+       my ($url, %extra) = @_;
+
+       my $p = "POST $url HTTP/1.1" . CRLF .
+               "Host: localhost" . CRLF .
+               "Transfer-Encoding: chunked" . CRLF .
+               "Connection: close" . CRLF .
+               CRLF .
+               "8" . CRLF .
+               "CHUNKED-" . CRLF .
+               "4" . CRLF .
+               "BODY" . CRLF .
+               "0" . CRLF .
+               CRLF;
+
+       return http($p, %extra);
+}
+
+sub http_post_chunked_too_large {
+       my ($url, %extra) = @_;
+
+       my $p = "POST $url HTTP/1.1" . CRLF .
+               "Host: localhost" . CRLF .
+               "Transfer-Encoding: chunked" . CRLF .
+               "Connection: close" . CRLF .
+               CRLF .
+               "8" . CRLF .
+               "TOOLARGE" . CRLF .
+               "0" . CRLF .
+               CRLF;
+
+       return http($p, %extra);
+}
+
+###############################################################################
index a30e02fe8158aac5d289ccd82f12f4a4bbdc87e2..f810bedc6830388978b2c42f2f16589071a341dc 100644 (file)
@@ -69,6 +69,15 @@ async function http_module(r: NginxHTTPRequest) {
     // r.requestBuffer
     r.requestBuffer?.equals(Buffer.from([1]));
 
+    // r.readRequestText
+    let text: string = await r.readRequestText();
+
+    // r.readRequestArrayBuffer
+    let buf: ArrayBuffer = await r.readRequestArrayBuffer();
+
+    // r.readRequestJSON
+    let json: any = await r.readRequestJSON();
+
     // r.responseText
     r.responseText == 'a';
     r.responseText?.startsWith('a');
index b0af0c5ab8a819c79cacd2558ca85e3851654f20..6f9a07c78ff6d227930b63a8be814bf24f05e664 100644 (file)
@@ -381,6 +381,47 @@ interface NginxHTTPRequest {
      * @deprecated Use `requestText` or `requestBuffer` instead.
      */
     readonly requestBody?: string;
+    /**
+     * Reads the client request body and returns a Promise resolving
+     * with the body as a string.
+     *
+     * Available in js_access and js_content directives.  The request body
+     * size is limited by client_max_body_size.
+     *
+     * The body is read once and cached on the request: subsequent
+     * `readRequestText`, `readRequestArrayBuffer`, and `readRequestJSON`
+     * calls resolve synchronously from the cache and do not re-read the
+     * wire.  This deliberately differs from the WHATWG Fetch Body mixin
+     * (which makes the body unusable after the first call) and matches
+     * the server-side caching pattern used by Express, Flask, and similar
+     * frameworks.
+     *
+     * A second call issued while a previous `readRequest*` promise has
+     * not yet resolved throws `"request body is already being read"`.
+     *
+     * @returns A Promise that resolves with the request body as a string.
+     * @since 0.9.9
+     */
+    readRequestText(): Promise<string>;
+    /**
+     * Reads the client request body and returns a Promise resolving
+     * with the body as an ArrayBuffer.  See {@link readRequestText} for
+     * caching, concurrency, and availability semantics.
+     *
+     * @returns A Promise that resolves with the request body
+     *   as an ArrayBuffer.
+     * @since 0.9.9
+     */
+    readRequestArrayBuffer(): Promise<ArrayBuffer>;
+    /**
+     * Reads the client request body and returns a Promise resolving
+     * with the body parsed as JSON.  See {@link readRequestText} for
+     * caching, concurrency, and availability semantics.
+     *
+     * @returns A Promise that resolves with the parsed JSON value.
+     * @since 0.9.9
+     */
+    readRequestJSON(): Promise<any>;
     /**
      * Subrequest response body. The size of response body is limited by
      * the subrequest_output_buffer_size directive.