1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "FetchStreamReader.h"
8 #include "InternalResponse.h"
10 #include "mozilla/ConsoleReportCollector.h"
11 #include "mozilla/dom/AutoEntryScript.h"
12 #include "mozilla/dom/DOMException.h"
13 #include "mozilla/dom/Promise.h"
14 #include "mozilla/dom/PromiseBinding.h"
15 #include "mozilla/dom/WorkerPrivate.h"
16 #include "mozilla/dom/WorkerRef.h"
17 #include "mozilla/HoldDropJSObjects.h"
18 #include "mozilla/TaskCategory.h"
19 #include "nsContentUtils.h"
20 #include "nsIAsyncInputStream.h"
22 #include "nsIScriptError.h"
23 #include "nsPIDOMWindow.h"
26 namespace mozilla::dom
{
28 NS_IMPL_CYCLE_COLLECTING_ADDREF(FetchStreamReader
)
29 NS_IMPL_CYCLE_COLLECTING_RELEASE(FetchStreamReader
)
31 NS_IMPL_CYCLE_COLLECTION_CLASS(FetchStreamReader
)
33 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(FetchStreamReader
)
34 NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal
)
35 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
37 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(FetchStreamReader
)
38 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal
)
39 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
41 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(FetchStreamReader
)
42 NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReader
)
43 NS_IMPL_CYCLE_COLLECTION_TRACE_END
45 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FetchStreamReader
)
46 NS_INTERFACE_MAP_ENTRY(nsIOutputStreamCallback
)
47 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports
, nsIOutputStreamCallback
)
51 nsresult
FetchStreamReader::Create(JSContext
* aCx
, nsIGlobalObject
* aGlobal
,
52 FetchStreamReader
** aStreamReader
,
53 nsIInputStream
** aInputStream
) {
56 MOZ_ASSERT(aStreamReader
);
57 MOZ_ASSERT(aInputStream
);
59 RefPtr
<FetchStreamReader
> streamReader
= new FetchStreamReader(aGlobal
);
61 nsCOMPtr
<nsIAsyncInputStream
> pipeIn
;
64 NS_NewPipe2(getter_AddRefs(pipeIn
),
65 getter_AddRefs(streamReader
->mPipeOut
), true, true, 0, 0);
66 if (NS_WARN_IF(NS_FAILED(rv
))) {
70 if (!NS_IsMainThread()) {
71 WorkerPrivate
* workerPrivate
= GetWorkerPrivateFromContext(aCx
);
72 MOZ_ASSERT(workerPrivate
);
74 RefPtr
<WeakWorkerRef
> workerRef
=
75 WeakWorkerRef::Create(workerPrivate
, [streamReader
]() {
76 MOZ_ASSERT(streamReader
);
77 MOZ_ASSERT(streamReader
->mWorkerRef
);
79 WorkerPrivate
* workerPrivate
= streamReader
->mWorkerRef
->GetPrivate();
80 MOZ_ASSERT(workerPrivate
);
82 streamReader
->CloseAndRelease(workerPrivate
->GetJSContext(),
83 NS_ERROR_DOM_INVALID_STATE_ERR
);
86 if (NS_WARN_IF(!workerRef
)) {
87 streamReader
->mPipeOut
->CloseWithStatus(NS_ERROR_DOM_INVALID_STATE_ERR
);
88 return NS_ERROR_DOM_INVALID_STATE_ERR
;
91 // These 2 objects create a ref-cycle here that is broken when the stream is
92 // closed or the worker shutsdown.
93 streamReader
->mWorkerRef
= std::move(workerRef
);
96 pipeIn
.forget(aInputStream
);
97 streamReader
.forget(aStreamReader
);
101 FetchStreamReader::FetchStreamReader(nsIGlobalObject
* aGlobal
)
103 mOwningEventTarget(mGlobal
->EventTargetFor(TaskCategory::Other
)),
106 mStreamClosed(false) {
109 mozilla::HoldJSObjects(this);
112 FetchStreamReader::~FetchStreamReader() {
113 CloseAndRelease(nullptr, NS_BASE_STREAM_CLOSED
);
115 mozilla::DropJSObjects(this);
118 // If a context is provided, an attempt will be made to cancel the reader. The
119 // only situation where we don't expect to have a context is when closure is
120 // being triggered from the destructor or the WorkerRef is notifying. If
121 // we're at the destructor, it's far too late to cancel anything. And if the
122 // WorkerRef is being notified, the global is going away, so there's also
123 // no need to do further JS work.
124 void FetchStreamReader::CloseAndRelease(JSContext
* aCx
, nsresult aStatus
) {
125 NS_ASSERT_OWNINGTHREAD(FetchStreamReader
);
132 RefPtr
<FetchStreamReader
> kungFuDeathGrip
= this;
134 if (aCx
&& mReader
) {
135 RefPtr
<DOMException
> error
= DOMException::Create(aStatus
);
137 JS::Rooted
<JS::Value
> errorValue(aCx
);
138 if (ToJSValue(aCx
, error
, &errorValue
)) {
139 JS::Rooted
<JSObject
*> reader(aCx
, mReader
);
140 // It's currently safe to cancel an already closed reader because, per the
141 // comments in ReadableStream::cancel() conveying the spec, step 2 of
142 // 3.4.3 that specified ReadableStreamCancel is: If stream.[[state]] is
143 // "closed", return a new promise resolved with undefined.
144 JS::ReadableStreamReaderCancel(aCx
, reader
, errorValue
);
147 // We don't want to propagate exceptions during the cleanup.
148 JS_ClearPendingException(aCx
);
151 mStreamClosed
= true;
155 mPipeOut
->CloseWithStatus(aStatus
);
158 mWorkerRef
= nullptr;
164 void FetchStreamReader::StartConsuming(JSContext
* aCx
, JS::HandleObject aStream
,
165 JS::MutableHandle
<JSObject
*> aReader
,
167 MOZ_DIAGNOSTIC_ASSERT(!mReader
);
168 MOZ_DIAGNOSTIC_ASSERT(aStream
);
170 aRv
.MightThrowJSException();
172 // Here, by spec, we can pick any global we want. Just to avoid extra
173 // cross-compartment steps, we want to create the reader in the same
174 // compartment of the owning Fetch Body object.
175 // The same global will be used to retrieve data from this reader.
176 JSAutoRealm
ar(aCx
, mGlobal
->GetGlobalJSObject());
178 JS::Rooted
<JSObject
*> reader(
179 aCx
, JS::ReadableStreamGetReader(aCx
, aStream
,
180 JS::ReadableStreamReaderMode::Default
));
182 aRv
.StealExceptionFromJSContext(aCx
);
183 CloseAndRelease(aCx
, NS_ERROR_DOM_INVALID_STATE_ERR
);
190 aRv
= mPipeOut
->AsyncWait(this, 0, 0, mOwningEventTarget
);
191 if (NS_WARN_IF(aRv
.Failed())) {
196 // nsIOutputStreamCallback interface
199 FetchStreamReader::OnOutputStreamReady(nsIAsyncOutputStream
* aStream
) {
200 NS_ASSERT_OWNINGTHREAD(FetchStreamReader
);
201 MOZ_ASSERT(aStream
== mPipeOut
);
208 if (!mBuffer
.IsEmpty()) {
209 return WriteBuffer();
212 // Here we can retrieve data from the reader using any global we want because
213 // it is not observable. We want to use the reader's global, which is also the
215 AutoEntryScript
aes(mGlobal
, "ReadableStreamReader.read", !mWorkerRef
);
217 JS::Rooted
<JSObject
*> reader(aes
.cx(), mReader
);
218 JS::Rooted
<JSObject
*> promise(
219 aes
.cx(), JS::ReadableStreamDefaultReaderRead(aes
.cx(), reader
));
220 if (NS_WARN_IF(!promise
)) {
221 // Let's close the stream.
222 CloseAndRelease(aes
.cx(), NS_ERROR_DOM_INVALID_STATE_ERR
);
223 return NS_ERROR_FAILURE
;
226 RefPtr
<Promise
> domPromise
= Promise::CreateFromExisting(mGlobal
, promise
);
227 if (NS_WARN_IF(!domPromise
)) {
228 // Let's close the stream.
229 CloseAndRelease(aes
.cx(), NS_ERROR_DOM_INVALID_STATE_ERR
);
230 return NS_ERROR_FAILURE
;
234 domPromise
->AppendNativeHandler(this);
238 void FetchStreamReader::ResolvedCallback(JSContext
* aCx
,
239 JS::Handle
<JS::Value
> aValue
) {
244 // This promise should be resolved with { done: boolean, value: something },
245 // "value" is interesting only if done is false.
247 // We don't want to play with JS api, let's WebIDL bindings doing it for us.
248 // FetchReadableStreamReadDataDone is a dictionary with just a boolean, if the
249 // parsing succeeded, we can proceed with the parsing of the "value", which it
250 // must be a Uint8Array.
251 FetchReadableStreamReadDataDone valueDone
;
252 if (!valueDone
.Init(aCx
, aValue
)) {
253 JS_ClearPendingException(aCx
);
254 CloseAndRelease(aCx
, NS_ERROR_DOM_INVALID_STATE_ERR
);
258 if (valueDone
.mDone
) {
259 // Stream is completed.
260 CloseAndRelease(aCx
, NS_BASE_STREAM_CLOSED
);
264 RootedDictionary
<FetchReadableStreamReadDataArray
> value(aCx
);
265 if (!value
.Init(aCx
, aValue
) || !value
.mValue
.WasPassed()) {
266 JS_ClearPendingException(aCx
);
267 CloseAndRelease(aCx
, NS_ERROR_DOM_INVALID_STATE_ERR
);
271 Uint8Array
& array
= value
.mValue
.Value();
272 array
.ComputeState();
273 uint32_t len
= array
.Length();
276 // If there is nothing to read, let's do another reading.
277 OnOutputStreamReady(mPipeOut
);
281 MOZ_DIAGNOSTIC_ASSERT(mBuffer
.IsEmpty());
283 // Let's take a copy of the data.
284 if (!mBuffer
.AppendElements(array
.Data(), len
, fallible
)) {
285 CloseAndRelease(aCx
, NS_ERROR_OUT_OF_MEMORY
);
290 mBufferRemaining
= len
;
292 nsresult rv
= WriteBuffer();
294 // DOMException only understands errors from domerr.msg, so we normalize to
295 // identifying an abort if the write fails.
296 CloseAndRelease(aCx
, NS_ERROR_DOM_ABORT_ERR
);
300 nsresult
FetchStreamReader::WriteBuffer() {
301 MOZ_ASSERT(!mBuffer
.IsEmpty());
303 char* data
= reinterpret_cast<char*>(mBuffer
.Elements());
306 uint32_t written
= 0;
308 mPipeOut
->Write(data
+ mBufferOffset
, mBufferRemaining
, &written
);
310 if (rv
== NS_BASE_STREAM_WOULD_BLOCK
) {
314 if (NS_WARN_IF(NS_FAILED(rv
))) {
318 MOZ_ASSERT(written
<= mBufferRemaining
);
319 mBufferRemaining
-= written
;
320 mBufferOffset
+= written
;
322 if (mBufferRemaining
== 0) {
328 nsresult rv
= mPipeOut
->AsyncWait(this, 0, 0, mOwningEventTarget
);
329 if (NS_WARN_IF(NS_FAILED(rv
))) {
336 void FetchStreamReader::RejectedCallback(JSContext
* aCx
,
337 JS::Handle
<JS::Value
> aValue
) {
338 ReportErrorToConsole(aCx
, aValue
);
339 CloseAndRelease(aCx
, NS_ERROR_FAILURE
);
342 void FetchStreamReader::ReportErrorToConsole(JSContext
* aCx
,
343 JS::Handle
<JS::Value
> aValue
) {
344 nsCString sourceSpec
;
347 nsString valueString
;
349 nsContentUtils::ExtractErrorValues(aCx
, aValue
, sourceSpec
, &line
, &column
,
352 nsTArray
<nsString
> params
;
353 params
.AppendElement(valueString
);
355 RefPtr
<ConsoleReportCollector
> reporter
= new ConsoleReportCollector();
356 reporter
->AddConsoleReport(nsIScriptError::errorFlag
,
357 "ReadableStreamReader.read"_ns
,
358 nsContentUtils::eDOM_PROPERTIES
, sourceSpec
, line
,
359 column
, "ReadableStreamReadingFailed"_ns
, params
);
361 uint64_t innerWindowId
= 0;
363 if (NS_IsMainThread()) {
364 nsCOMPtr
<nsPIDOMWindowInner
> window
= do_QueryInterface(mGlobal
);
366 innerWindowId
= window
->WindowID();
368 reporter
->FlushReportsToConsole(innerWindowId
);
372 WorkerPrivate
* workerPrivate
= GetWorkerPrivateFromContext(aCx
);
374 innerWindowId
= workerPrivate
->WindowID();
377 RefPtr
<Runnable
> r
= NS_NewRunnableFunction(
378 "FetchStreamReader::ReportErrorToConsole", [reporter
, innerWindowId
]() {
379 reporter
->FlushReportsToConsole(innerWindowId
);
382 workerPrivate
->DispatchToMainThread(r
.forget());
385 } // namespace mozilla::dom