bool body_reader_then_handler(JSContext *cx, HandleObject body_owner, HandleValue extra,
                              CallArgs args) {
  RootedObject then_handler(cx, &args.callee());
  // The reader is stored in the catch handler, which we need here as well.
  // So we get that first, then the reader.
  RootedObject catch_handler(cx, &extra.toObject());
  RootedObject reader(cx, &js::GetFunctionNativeReserved(catch_handler, 1).toObject());
  BodyHandle body_handle = RequestOrResponse::body_handle(body_owner);

  // We're guaranteed to work with a native ReadableStreamDefaultReader here,
  // which in turn is guaranteed to vend {done: bool, value: any} objects to
  // read promise then callbacks.
  RootedObject chunk_obj(cx, &args[0].toObject());
  RootedValue done_val(cx);
  if (!JS_GetProperty(cx, chunk_obj, "done", &done_val))
    return false;

  if (done_val.toBoolean()) {
    // The only response we ever send is the one passed to
    // `FetchEvent#respondWith` to send to the client. As such, we can be
    // certain that if we have a response here, we can advance the FetchState to
    // `responseDone`.
    if (Response::is_instance(body_owner)) {
      FetchEvent::set_state(FetchEvent::instance(), FetchEvent::State::responseDone);
    }

    if (!HANDLE_RESULT(cx, xqd_body_close(body_handle))) {
      return false;
    }

    if (Request::is_instance(body_owner)) {
      if (!pending_requests->append(body_owner)) {
        return false;
      }
    }

    return true;
  }

  RootedValue val(cx);
  if (!JS_GetProperty(cx, chunk_obj, "value", &val))
    return false;

  // The read operation returned something that's not a Uint8Array
  if (!val.isObject() || !JS_IsUint8Array(&val.toObject())) {
    // reject the request promise
    if (Request::is_instance(body_owner)) {
      RootedObject response_promise(cx, Request::response_promise(body_owner));
      RootedValue exn(cx);

      // TODO: this should be a TypeError, but I'm not sure how to make that work
      JS_ReportErrorUTF8(cx, "TypeError");
      if (!JS_GetPendingException(cx, &exn)) {
        return false;
      }
      JS_ClearPendingException(cx);

      return JS::RejectPromise(cx, response_promise, exn);
    }

    // TODO: should we also create a rejected promise if a response reads something that's not a
    // Uint8Array?
    fprintf(stderr, "Error: read operation on body ReadableStream didn't respond with a "
                    "Uint8Array. Received value: ");
    dump_value(cx, val, stderr);
    return false;
  }

  int result;
  {
    JS::AutoCheckCannotGC nogc;
    JSObject *array = &val.toObject();
    bool is_shared;
    uint8_t *bytes = JS_GetUint8ArrayData(array, &is_shared, nogc);
    size_t length = JS_GetTypedArrayByteLength(array);
    result = write_to_body_all(body_handle, (char *)bytes, length);
  }

  // Needs to be outside the nogc block in case we need to create an exception.
  if (!HANDLE_RESULT(cx, result)) {
    return false;
  }

  // Read the next chunk.
  RootedObject promise(cx, JS::ReadableStreamDefaultReaderRead(cx, reader));
  if (!promise)
    return false;
  return JS::AddPromiseReactions(cx, promise, then_handler, catch_handler);
}
bool body_reader_then_handler(JSContext *cx, HandleObject body_owner, HandleValue extra,
                              CallArgs args) {
  RootedObject then_handler(cx, &args.callee());
   
   
  RootedObject catch_handler(cx, &extra.toObject());
  RootedObject reader(cx, &js::GetFunctionNativeReserved(catch_handler, 1).toObject());
  BodyHandle body_handle = RequestOrResponse::body_handle(body_owner);

   
   
   
  RootedObject chunk_obj(cx, &args[0].toObject());
  RootedValue done_val(cx);
  if (!JS_GetProperty(cx, chunk_obj, "done", &done_val))
    return false;

  if (done_val.toBoolean()) {
     
     
     
     
    if (Response::is_instance(body_owner)) {
      FetchEvent::set_state(FetchEvent::instance(), FetchEvent::State::responseDone);
    }

    if (!HANDLE_RESULT(cx, xqd_body_close(body_handle))) {
      return false;
    }

    if (Request::is_instance(body_owner)) {
      if (!pending_requests->append(body_owner)) {
        return false;
      }
    }

    return true;
  }

  RootedValue val(cx);
  if (!JS_GetProperty(cx, chunk_obj, "value", &val))
    return false;

   
  if (!val.isObject() || !JS_IsUint8Array(&val.toObject())) {
     
    if (Request::is_instance(body_owner)) {
      RootedObject response_promise(cx, Request::response_promise(body_owner));
      RootedValue exn(cx);

       
      JS_ReportErrorUTF8(cx, "TypeError");
      if (!JS_GetPendingException(cx, &exn)) {
        return false;
      }
      JS_ClearPendingException(cx);

      return JS::RejectPromise(cx, response_promise, exn);
    }

     
     
    fprintf(stderr, "Error: read operation on body ReadableStream didn't respond with a "
                    "Uint8Array. Received value: ");
    dump_value(cx, val, stderr);
    return false;
  }

  int result;
  {
    JS::AutoCheckCannotGC nogc;
    JSObject *array = &val.toObject();
    bool is_shared;
    uint8_t *bytes = JS_GetUint8ArrayData(array, &is_shared, nogc);
    size_t length = JS_GetTypedArrayByteLength(array);
    result = write_to_body_all(body_handle, (char *)bytes, length);
  }

   
  if (!HANDLE_RESULT(cx, result)) {
    return false;
  }

   
  RootedObject promise(cx, JS::ReadableStreamDefaultReaderRead(cx, reader));
  if (!promise)
    return false;
  return JS::AddPromiseReactions(cx, promise, then_handler, catch_handler);
}