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;
};
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);
}
},
+ {
+ .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"),
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)),
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;
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;
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;
"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);
}
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);
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]);
}
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) ),
};
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) ),
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
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);
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);
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");
--- /dev/null
+#!/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);
+}
+
+###############################################################################