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/. */
6
7 #include "FetchStreamReader.h"
8 #include "InternalResponse.h"
9 #include "mozilla/dom/PromiseBinding.h"
10 #include "mozilla/SystemGroup.h"
11 #include "mozilla/TaskCategory.h"
12 #include "nsContentUtils.h"
13 #include "nsIScriptError.h"
14 #include "nsPIDOMWindow.h"
15
16 namespace mozilla {
17 namespace dom {
18
19 namespace {
20
21 class FetchStreamReaderWorkerHolder final : public WorkerHolder {
22 public:
FetchStreamReaderWorkerHolder(FetchStreamReader * aReader)23 explicit FetchStreamReaderWorkerHolder(FetchStreamReader* aReader)
24 : WorkerHolder("FetchStreamReaderWorkerHolder",
25 WorkerHolder::Behavior::AllowIdleShutdownStart),
26 mReader(aReader),
27 mWasNotified(false) {}
28
Notify(WorkerStatus aStatus)29 bool Notify(WorkerStatus aStatus) override {
30 if (!mWasNotified) {
31 mWasNotified = true;
32 // The WorkerPrivate does have a context available, and we could pass it
33 // here to trigger cancellation of the reader, but the author of this
34 // comment chickened out.
35 mReader->CloseAndRelease(nullptr, NS_ERROR_DOM_INVALID_STATE_ERR);
36 }
37
38 return true;
39 }
40
41 private:
42 RefPtr<FetchStreamReader> mReader;
43 bool mWasNotified;
44 };
45
46 } // namespace
47
48 NS_IMPL_CYCLE_COLLECTING_ADDREF(FetchStreamReader)
NS_IMPL_CYCLE_COLLECTING_RELEASE(FetchStreamReader)49 NS_IMPL_CYCLE_COLLECTING_RELEASE(FetchStreamReader)
50
51 NS_IMPL_CYCLE_COLLECTION_CLASS(FetchStreamReader)
52
53 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(FetchStreamReader)
54 NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal)
55 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
56
57 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(FetchStreamReader)
58 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal)
59 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
60
61 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(FetchStreamReader)
62 NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReader)
63 NS_IMPL_CYCLE_COLLECTION_TRACE_END
64
65 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FetchStreamReader)
66 NS_INTERFACE_MAP_ENTRY(nsIOutputStreamCallback)
67 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIOutputStreamCallback)
68 NS_INTERFACE_MAP_END
69
70 /* static */ nsresult FetchStreamReader::Create(
71 JSContext* aCx, nsIGlobalObject* aGlobal, FetchStreamReader** aStreamReader,
72 nsIInputStream** aInputStream) {
73 MOZ_ASSERT(aCx);
74 MOZ_ASSERT(aGlobal);
75 MOZ_ASSERT(aStreamReader);
76 MOZ_ASSERT(aInputStream);
77
78 RefPtr<FetchStreamReader> streamReader = new FetchStreamReader(aGlobal);
79
80 nsCOMPtr<nsIAsyncInputStream> pipeIn;
81
82 nsresult rv =
83 NS_NewPipe2(getter_AddRefs(pipeIn),
84 getter_AddRefs(streamReader->mPipeOut), true, true, 0, 0);
85 if (NS_WARN_IF(NS_FAILED(rv))) {
86 return rv;
87 }
88
89 if (!NS_IsMainThread()) {
90 WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx);
91 MOZ_ASSERT(workerPrivate);
92
93 // We need to know when the worker goes away.
94 UniquePtr<FetchStreamReaderWorkerHolder> holder(
95 new FetchStreamReaderWorkerHolder(streamReader));
96 if (NS_WARN_IF(!holder->HoldWorker(workerPrivate, Closing))) {
97 streamReader->mPipeOut->CloseWithStatus(NS_ERROR_DOM_INVALID_STATE_ERR);
98 return NS_ERROR_DOM_INVALID_STATE_ERR;
99 }
100
101 // These 2 objects create a ref-cycle here that is broken when the stream is
102 // closed or the worker shutsdown.
103 streamReader->mWorkerHolder = Move(holder);
104 }
105
106 pipeIn.forget(aInputStream);
107 streamReader.forget(aStreamReader);
108 return NS_OK;
109 }
110
FetchStreamReader(nsIGlobalObject * aGlobal)111 FetchStreamReader::FetchStreamReader(nsIGlobalObject* aGlobal)
112 : mGlobal(aGlobal),
113 mOwningEventTarget(mGlobal->EventTargetFor(TaskCategory::Other)),
114 mBufferRemaining(0),
115 mBufferOffset(0),
116 mStreamClosed(false) {
117 MOZ_ASSERT(aGlobal);
118 }
119
~FetchStreamReader()120 FetchStreamReader::~FetchStreamReader() {
121 CloseAndRelease(nullptr, NS_BASE_STREAM_CLOSED);
122 }
123
124 // If a context is provided, an attempt will be made to cancel the reader. The
125 // only situation where we don't expect to have a context is when closure is
126 // being triggered from the destructor or the WorkerHolder is notifying. If
127 // we're at the destructor, it's far too late to cancel anything. And if the
128 // WorkerHolder is being notified, the global is going away, so there's also
129 // no need to do further JS work.
CloseAndRelease(JSContext * aCx,nsresult aStatus)130 void FetchStreamReader::CloseAndRelease(JSContext* aCx, nsresult aStatus) {
131 NS_ASSERT_OWNINGTHREAD(FetchStreamReader);
132
133 if (mStreamClosed) {
134 // Already closed.
135 return;
136 }
137
138 RefPtr<FetchStreamReader> kungFuDeathGrip = this;
139
140 if (aCx) {
141 MOZ_ASSERT(mReader);
142
143 RefPtr<DOMException> error = DOMException::Create(aStatus);
144
145 JS::Rooted<JS::Value> errorValue(aCx);
146 if (ToJSValue(aCx, error, &errorValue)) {
147 JS::Rooted<JSObject*> reader(aCx, mReader);
148 // It's currently safe to cancel an already closed reader because, per the
149 // comments in ReadableStream::cancel() conveying the spec, step 2 of
150 // 3.4.3 that specified ReadableStreamCancel is: If stream.[[state]] is
151 // "closed", return a new promise resolved with undefined.
152 JS::ReadableStreamReaderCancel(aCx, reader, errorValue);
153 }
154 }
155
156 mStreamClosed = true;
157
158 mGlobal = nullptr;
159
160 mPipeOut->CloseWithStatus(aStatus);
161 mPipeOut = nullptr;
162
163 mWorkerHolder = nullptr;
164
165 mReader = nullptr;
166 mBuffer = nullptr;
167 }
168
StartConsuming(JSContext * aCx,JS::HandleObject aStream,JS::MutableHandle<JSObject * > aReader,ErrorResult & aRv)169 void FetchStreamReader::StartConsuming(JSContext* aCx, JS::HandleObject aStream,
170 JS::MutableHandle<JSObject*> aReader,
171 ErrorResult& aRv) {
172 MOZ_DIAGNOSTIC_ASSERT(!mReader);
173 MOZ_DIAGNOSTIC_ASSERT(aStream);
174
175 JS::Rooted<JSObject*> reader(
176 aCx, JS::ReadableStreamGetReader(aCx, aStream,
177 JS::ReadableStreamReaderMode::Default));
178 if (!reader) {
179 aRv.StealExceptionFromJSContext(aCx);
180 CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR);
181 return;
182 }
183
184 mReader = reader;
185 aReader.set(reader);
186
187 aRv = mPipeOut->AsyncWait(this, 0, 0, mOwningEventTarget);
188 if (NS_WARN_IF(aRv.Failed())) {
189 return;
190 }
191 }
192
193 // nsIOutputStreamCallback interface
194
195 NS_IMETHODIMP
OnOutputStreamReady(nsIAsyncOutputStream * aStream)196 FetchStreamReader::OnOutputStreamReady(nsIAsyncOutputStream* aStream) {
197 NS_ASSERT_OWNINGTHREAD(FetchStreamReader);
198 MOZ_ASSERT(aStream == mPipeOut);
199 MOZ_ASSERT(mReader);
200
201 if (mStreamClosed) {
202 return NS_OK;
203 }
204
205 if (mBuffer) {
206 return WriteBuffer();
207 }
208
209 // TODO: We need to verify this is the correct global per the spec.
210 // See bug 1385890.
211 AutoEntryScript aes(mGlobal, "ReadableStreamReader.read", !mWorkerHolder);
212
213 JS::Rooted<JSObject*> reader(aes.cx(), mReader);
214 JS::Rooted<JSObject*> promise(
215 aes.cx(), JS::ReadableStreamDefaultReaderRead(aes.cx(), reader));
216 if (NS_WARN_IF(!promise)) {
217 // Let's close the stream.
218 CloseAndRelease(aes.cx(), NS_ERROR_DOM_INVALID_STATE_ERR);
219 return NS_ERROR_FAILURE;
220 }
221
222 RefPtr<Promise> domPromise = Promise::CreateFromExisting(mGlobal, promise);
223 if (NS_WARN_IF(!domPromise)) {
224 // Let's close the stream.
225 CloseAndRelease(aes.cx(), NS_ERROR_DOM_INVALID_STATE_ERR);
226 return NS_ERROR_FAILURE;
227 }
228
229 // Let's wait.
230 domPromise->AppendNativeHandler(this);
231 return NS_OK;
232 }
233
ResolvedCallback(JSContext * aCx,JS::Handle<JS::Value> aValue)234 void FetchStreamReader::ResolvedCallback(JSContext* aCx,
235 JS::Handle<JS::Value> aValue) {
236 if (mStreamClosed) {
237 return;
238 }
239
240 // This promise should be resolved with { done: boolean, value: something },
241 // "value" is interesting only if done is false.
242
243 // We don't want to play with JS api, let's WebIDL bindings doing it for us.
244 // FetchReadableStreamReadDataDone is a dictionary with just a boolean, if the
245 // parsing succeeded, we can proceed with the parsing of the "value", which it
246 // must be a Uint8Array.
247 FetchReadableStreamReadDataDone valueDone;
248 if (!valueDone.Init(aCx, aValue)) {
249 JS_ClearPendingException(aCx);
250 CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR);
251 return;
252 }
253
254 if (valueDone.mDone) {
255 // Stream is completed.
256 CloseAndRelease(aCx, NS_BASE_STREAM_CLOSED);
257 return;
258 }
259
260 UniquePtr<FetchReadableStreamReadDataArray> value(
261 new FetchReadableStreamReadDataArray);
262 if (!value->Init(aCx, aValue) || !value->mValue.WasPassed()) {
263 JS_ClearPendingException(aCx);
264 CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR);
265 return;
266 }
267
268 Uint8Array& array = value->mValue.Value();
269 array.ComputeLengthAndData();
270 uint32_t len = array.Length();
271
272 if (len == 0) {
273 // If there is nothing to read, let's do another reading.
274 OnOutputStreamReady(mPipeOut);
275 return;
276 }
277
278 MOZ_DIAGNOSTIC_ASSERT(!mBuffer);
279 mBuffer = Move(value);
280
281 mBufferOffset = 0;
282 mBufferRemaining = len;
283
284 nsresult rv = WriteBuffer();
285 if (NS_FAILED(rv)) {
286 // DOMException only understands errors from domerr.msg, so we normalize to
287 // identifying an abort if the write fails.
288 CloseAndRelease(aCx, NS_ERROR_DOM_ABORT_ERR);
289 }
290 }
291
WriteBuffer()292 nsresult FetchStreamReader::WriteBuffer() {
293 MOZ_ASSERT(mBuffer);
294 MOZ_ASSERT(mBuffer->mValue.WasPassed());
295
296 Uint8Array& array = mBuffer->mValue.Value();
297 char* data = reinterpret_cast<char*>(array.Data());
298
299 while (1) {
300 uint32_t written = 0;
301 nsresult rv =
302 mPipeOut->Write(data + mBufferOffset, mBufferRemaining, &written);
303
304 if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
305 break;
306 }
307
308 if (NS_WARN_IF(NS_FAILED(rv))) {
309 return rv;
310 }
311
312 MOZ_ASSERT(written <= mBufferRemaining);
313 mBufferRemaining -= written;
314 mBufferOffset += written;
315
316 if (mBufferRemaining == 0) {
317 mBuffer = nullptr;
318 break;
319 }
320 }
321
322 nsresult rv = mPipeOut->AsyncWait(this, 0, 0, mOwningEventTarget);
323 if (NS_WARN_IF(NS_FAILED(rv))) {
324 return rv;
325 }
326
327 return NS_OK;
328 }
329
RejectedCallback(JSContext * aCx,JS::Handle<JS::Value> aValue)330 void FetchStreamReader::RejectedCallback(JSContext* aCx,
331 JS::Handle<JS::Value> aValue) {
332 ReportErrorToConsole(aCx, aValue);
333 CloseAndRelease(aCx, NS_ERROR_FAILURE);
334 }
335
ReportErrorToConsole(JSContext * aCx,JS::Handle<JS::Value> aValue)336 void FetchStreamReader::ReportErrorToConsole(JSContext* aCx,
337 JS::Handle<JS::Value> aValue) {
338 nsCString sourceSpec;
339 uint32_t line = 0;
340 uint32_t column = 0;
341 nsString valueString;
342
343 nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column,
344 valueString);
345
346 nsTArray<nsString> params;
347 params.AppendElement(valueString);
348
349 RefPtr<ConsoleReportCollector> reporter = new ConsoleReportCollector();
350 reporter->AddConsoleReport(
351 nsIScriptError::errorFlag,
352 NS_LITERAL_CSTRING("ReadableStreamReader.read"),
353 nsContentUtils::eDOM_PROPERTIES, sourceSpec, line, column,
354 NS_LITERAL_CSTRING("ReadableStreamReadingFailed"), params);
355
356 uint64_t innerWindowId = 0;
357
358 if (NS_IsMainThread()) {
359 nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal);
360 if (window) {
361 innerWindowId = window->WindowID();
362 }
363 reporter->FlushReportsToConsole(innerWindowId);
364 return;
365 }
366
367 WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx);
368 if (workerPrivate) {
369 innerWindowId = workerPrivate->WindowID();
370 }
371
372 RefPtr<Runnable> r = NS_NewRunnableFunction(
373 "FetchStreamReader::ReportErrorToConsole", [reporter, innerWindowId]() {
374 reporter->FlushReportsToConsole(innerWindowId);
375 });
376
377 workerPrivate->DispatchToMainThread(r.forget());
378 }
379
380 } // namespace dom
381 } // namespace mozilla
382