1 | /* |
2 | * Copyright (C) 2010 Google Inc. All rights reserved. |
3 | * Copyright (C) 2016 Igalia S.L. |
4 | * |
5 | * Redistribution and use in source and binary forms, with or without |
6 | * modification, are permitted provided that the following conditions are |
7 | * met: |
8 | * |
9 | * * Redistributions of source code must retain the above copyright |
10 | * notice, this list of conditions and the following disclaimer. |
11 | * * Redistributions in binary form must reproduce the above |
12 | * copyright notice, this list of conditions and the following disclaimer |
13 | * in the documentation and/or other materials provided with the |
14 | * distribution. |
15 | * * Neither the name of Google Inc. nor the names of its |
16 | * contributors may be used to endorse or promote products derived from |
17 | * this software without specific prior written permission. |
18 | * |
19 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
20 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
21 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
22 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
23 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
24 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
25 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
26 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
27 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
28 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
29 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
30 | */ |
31 | |
32 | #include "config.h" |
33 | #include "NetworkDataTaskBlob.h" |
34 | |
35 | #include "DataReference.h" |
36 | #include "Download.h" |
37 | #include "Logging.h" |
38 | #include "NetworkProcess.h" |
39 | #include "NetworkSession.h" |
40 | #include "WebErrors.h" |
41 | #include <WebCore/AsyncFileStream.h> |
42 | #include <WebCore/BlobRegistryImpl.h> |
43 | #include <WebCore/HTTPParsers.h> |
44 | #include <WebCore/ParsedContentRange.h> |
45 | #include <WebCore/ResourceError.h> |
46 | #include <WebCore/ResourceResponse.h> |
47 | #include <WebCore/SharedBuffer.h> |
48 | #include <wtf/RunLoop.h> |
49 | |
50 | namespace WebKit { |
51 | using namespace WebCore; |
52 | |
53 | static const unsigned bufferSize = 512 * 1024; |
54 | |
55 | static const int httpOK = 200; |
56 | static const int httpPartialContent = 206; |
57 | static const int httpNotAllowed = 403; |
58 | static const int httpRequestedRangeNotSatisfiable = 416; |
59 | static const int httpInternalError = 500; |
60 | static const char* httpOKText = "OK" ; |
61 | static const char* httpPartialContentText = "Partial Content" ; |
62 | static const char* httpNotAllowedText = "Not Allowed" ; |
63 | static const char* httpRequestedRangeNotSatisfiableText = "Requested Range Not Satisfiable" ; |
64 | static const char* httpInternalErrorText = "Internal Server Error" ; |
65 | |
66 | static const char* const webKitBlobResourceDomain = "WebKitBlobResource" ; |
67 | |
68 | NetworkDataTaskBlob::NetworkDataTaskBlob(NetworkSession& session, BlobRegistryImpl& blobRegistry, NetworkDataTaskClient& client, const ResourceRequest& request, ContentSniffingPolicy shouldContentSniff, const Vector<RefPtr<WebCore::BlobDataFileReference>>& fileReferences) |
69 | : NetworkDataTask(session, client, request, StoredCredentialsPolicy::DoNotUse, false, false) |
70 | , m_stream(std::make_unique<AsyncFileStream>(*this)) |
71 | , m_fileReferences(fileReferences) |
72 | , m_networkProcess(session.networkProcess()) |
73 | { |
74 | for (auto& fileReference : m_fileReferences) |
75 | fileReference->prepareForFileAccess(); |
76 | |
77 | m_blobData = blobRegistry.getBlobDataFromURL(request.url()); |
78 | |
79 | m_session->registerNetworkDataTask(*this); |
80 | LOG(NetworkSession, "%p - Created NetworkDataTaskBlob for %s" , this, request.url().string().utf8().data()); |
81 | } |
82 | |
83 | NetworkDataTaskBlob::~NetworkDataTaskBlob() |
84 | { |
85 | for (auto& fileReference : m_fileReferences) |
86 | fileReference->revokeFileAccess(); |
87 | |
88 | clearStream(); |
89 | m_session->unregisterNetworkDataTask(*this); |
90 | } |
91 | |
92 | void NetworkDataTaskBlob::clearStream() |
93 | { |
94 | if (m_state == State::Completed) |
95 | return; |
96 | |
97 | m_state = State::Completed; |
98 | |
99 | if (m_fileOpened) { |
100 | m_fileOpened = false; |
101 | m_stream->close(); |
102 | } |
103 | m_stream = nullptr; |
104 | } |
105 | |
106 | void NetworkDataTaskBlob::resume() |
107 | { |
108 | ASSERT(m_state != State::Running); |
109 | if (m_state == State::Canceling || m_state == State::Completed) |
110 | return; |
111 | |
112 | m_state = State::Running; |
113 | |
114 | if (m_scheduledFailureType != NoFailure) { |
115 | ASSERT(m_failureTimer.isActive()); |
116 | return; |
117 | } |
118 | |
119 | RunLoop::main().dispatch([this, protectedThis = makeRef(*this)] { |
120 | if (m_state == State::Canceling || m_state == State::Completed || !m_client) { |
121 | clearStream(); |
122 | return; |
123 | } |
124 | |
125 | if (!equalLettersIgnoringASCIICase(m_firstRequest.httpMethod(), "get" )) { |
126 | didFail(Error::MethodNotAllowed); |
127 | return; |
128 | } |
129 | |
130 | // If the blob data is not found, fail now. |
131 | if (!m_blobData) { |
132 | didFail(Error::NotFoundError); |
133 | return; |
134 | } |
135 | |
136 | // Parse the "Range" header we care about. |
137 | String range = m_firstRequest.httpHeaderField(HTTPHeaderName::Range); |
138 | if (!range.isEmpty() && !parseRange(range, m_rangeOffset, m_rangeEnd, m_rangeSuffixLength)) { |
139 | dispatchDidReceiveResponse(Error::RangeError); |
140 | return; |
141 | } |
142 | |
143 | getSizeForNext(); |
144 | }); |
145 | } |
146 | |
147 | void NetworkDataTaskBlob::cancel() |
148 | { |
149 | if (m_state == State::Canceling || m_state == State::Completed) |
150 | return; |
151 | |
152 | m_state = State::Canceling; |
153 | |
154 | if (m_fileOpened) { |
155 | m_fileOpened = false; |
156 | m_stream->close(); |
157 | } |
158 | |
159 | if (isDownload()) |
160 | cleanDownloadFiles(); |
161 | } |
162 | |
163 | void NetworkDataTaskBlob::invalidateAndCancel() |
164 | { |
165 | cancel(); |
166 | clearStream(); |
167 | } |
168 | |
169 | void NetworkDataTaskBlob::getSizeForNext() |
170 | { |
171 | ASSERT(RunLoop::isMain()); |
172 | |
173 | // Do we finish validating and counting size for all items? |
174 | if (m_sizeItemCount >= m_blobData->items().size()) { |
175 | seek(); |
176 | dispatchDidReceiveResponse(); |
177 | return; |
178 | } |
179 | |
180 | const BlobDataItem& item = m_blobData->items().at(m_sizeItemCount); |
181 | switch (item.type()) { |
182 | case BlobDataItem::Type::Data: |
183 | didGetSize(item.length()); |
184 | break; |
185 | case BlobDataItem::Type::File: |
186 | // Files know their sizes, but asking the stream to verify that the file wasn't modified. |
187 | m_stream->getSize(item.file()->path(), item.file()->expectedModificationTime()); |
188 | break; |
189 | default: |
190 | ASSERT_NOT_REACHED(); |
191 | } |
192 | } |
193 | |
194 | void NetworkDataTaskBlob::didGetSize(long long size) |
195 | { |
196 | ASSERT(RunLoop::isMain()); |
197 | |
198 | if (m_state == State::Canceling || m_state == State::Completed || (!m_client && !isDownload())) { |
199 | clearStream(); |
200 | return; |
201 | } |
202 | |
203 | // If the size is -1, it means the file has been moved or changed. Fail now. |
204 | if (size == -1) { |
205 | didFail(Error::NotFoundError); |
206 | return; |
207 | } |
208 | |
209 | // The size passed back is the size of the whole file. If the underlying item is a sliced file, we need to use the slice length. |
210 | const BlobDataItem& item = m_blobData->items().at(m_sizeItemCount); |
211 | size = item.length(); |
212 | |
213 | // Cache the size. |
214 | m_itemLengthList.append(size); |
215 | |
216 | // Count the size. |
217 | m_totalSize += size; |
218 | m_totalRemainingSize += size; |
219 | m_sizeItemCount++; |
220 | |
221 | // Continue with the next item. |
222 | getSizeForNext(); |
223 | } |
224 | |
225 | void NetworkDataTaskBlob::seek() |
226 | { |
227 | ASSERT(RunLoop::isMain()); |
228 | |
229 | // Convert from the suffix length to the range. |
230 | if (m_rangeSuffixLength != kPositionNotSpecified) { |
231 | m_rangeOffset = m_totalRemainingSize - m_rangeSuffixLength; |
232 | m_rangeEnd = m_rangeOffset + m_rangeSuffixLength - 1; |
233 | } |
234 | |
235 | // Bail out if the range is not provided. |
236 | if (m_rangeOffset == kPositionNotSpecified) |
237 | return; |
238 | |
239 | // Skip the initial items that are not in the range. |
240 | long long offset = m_rangeOffset; |
241 | for (m_readItemCount = 0; m_readItemCount < m_blobData->items().size() && offset >= m_itemLengthList[m_readItemCount]; ++m_readItemCount) |
242 | offset -= m_itemLengthList[m_readItemCount]; |
243 | |
244 | // Set the offset that need to jump to for the first item in the range. |
245 | m_currentItemReadSize = offset; |
246 | |
247 | // Adjust the total remaining size in order not to go beyond the range. |
248 | if (m_rangeEnd != kPositionNotSpecified) { |
249 | long long rangeSize = m_rangeEnd - m_rangeOffset + 1; |
250 | if (m_totalRemainingSize > rangeSize) |
251 | m_totalRemainingSize = rangeSize; |
252 | } else |
253 | m_totalRemainingSize -= m_rangeOffset; |
254 | } |
255 | |
256 | void NetworkDataTaskBlob::dispatchDidReceiveResponse(Error errorCode) |
257 | { |
258 | LOG(NetworkSession, "%p - NetworkDataTaskBlob::dispatchDidReceiveResponse(%u)" , this, static_cast<unsigned>(errorCode)); |
259 | |
260 | Ref<NetworkDataTaskBlob> protectedThis(*this); |
261 | ResourceResponse response(m_firstRequest.url(), errorCode != Error::NoError ? "text/plain" : m_blobData->contentType(), errorCode != Error::NoError ? 0 : m_totalRemainingSize, String()); |
262 | switch (errorCode) { |
263 | case Error::NoError: { |
264 | bool isRangeRequest = m_rangeOffset != kPositionNotSpecified; |
265 | response.setHTTPStatusCode(isRangeRequest ? httpPartialContent : httpOK); |
266 | response.setHTTPStatusText(isRangeRequest ? httpPartialContentText : httpOKText); |
267 | |
268 | response.setHTTPHeaderField(HTTPHeaderName::ContentType, m_blobData->contentType()); |
269 | response.setHTTPHeaderField(HTTPHeaderName::ContentLength, String::number(m_totalRemainingSize)); |
270 | |
271 | if (isRangeRequest) |
272 | response.setHTTPHeaderField(HTTPHeaderName::ContentRange, ParsedContentRange(m_rangeOffset, m_rangeEnd, m_totalSize).headerValue()); |
273 | // FIXME: If a resource identified with a blob: URL is a File object, user agents must use that file's name attribute, |
274 | // as if the response had a Content-Disposition header with the filename parameter set to the File's name attribute. |
275 | // Notably, this will affect a name suggested in "File Save As". |
276 | break; |
277 | } |
278 | case Error::RangeError: |
279 | response.setHTTPStatusCode(httpRequestedRangeNotSatisfiable); |
280 | response.setHTTPStatusText(httpRequestedRangeNotSatisfiableText); |
281 | break; |
282 | case Error::SecurityError: |
283 | response.setHTTPStatusCode(httpNotAllowed); |
284 | response.setHTTPStatusText(httpNotAllowedText); |
285 | break; |
286 | default: |
287 | response.setHTTPStatusCode(httpInternalError); |
288 | response.setHTTPStatusText(httpInternalErrorText); |
289 | break; |
290 | } |
291 | |
292 | didReceiveResponse(WTFMove(response), [this, protectedThis = WTFMove(protectedThis), errorCode](PolicyAction policyAction) { |
293 | LOG(NetworkSession, "%p - NetworkDataTaskBlob::didReceiveResponse completionHandler (%u)" , this, static_cast<unsigned>(policyAction)); |
294 | |
295 | if (m_state == State::Canceling || m_state == State::Completed) { |
296 | clearStream(); |
297 | return; |
298 | } |
299 | |
300 | if (errorCode != Error::NoError) { |
301 | didFinish(); |
302 | return; |
303 | } |
304 | |
305 | switch (policyAction) { |
306 | case PolicyAction::Use: |
307 | m_buffer.resize(bufferSize); |
308 | read(); |
309 | break; |
310 | case PolicyAction::StopAllLoads: |
311 | ASSERT_NOT_REACHED(); |
312 | break; |
313 | case PolicyAction::Ignore: |
314 | break; |
315 | case PolicyAction::Download: |
316 | download(); |
317 | break; |
318 | } |
319 | }); |
320 | } |
321 | |
322 | void NetworkDataTaskBlob::read() |
323 | { |
324 | ASSERT(RunLoop::isMain()); |
325 | |
326 | // If there is no more remaining data to read, we are done. |
327 | if (!m_totalRemainingSize || m_readItemCount >= m_blobData->items().size()) { |
328 | didFinish(); |
329 | return; |
330 | } |
331 | |
332 | const BlobDataItem& item = m_blobData->items().at(m_readItemCount); |
333 | if (item.type() == BlobDataItem::Type::Data) |
334 | readData(item); |
335 | else if (item.type() == BlobDataItem::Type::File) |
336 | readFile(item); |
337 | else |
338 | ASSERT_NOT_REACHED(); |
339 | } |
340 | |
341 | void NetworkDataTaskBlob::readData(const BlobDataItem& item) |
342 | { |
343 | ASSERT(item.data().data()); |
344 | |
345 | long long bytesToRead = item.length() - m_currentItemReadSize; |
346 | if (bytesToRead > m_totalRemainingSize) |
347 | bytesToRead = m_totalRemainingSize; |
348 | consumeData(reinterpret_cast<const char*>(item.data().data()->data()) + item.offset() + m_currentItemReadSize, static_cast<int>(bytesToRead)); |
349 | m_currentItemReadSize = 0; |
350 | } |
351 | |
352 | void NetworkDataTaskBlob::readFile(const BlobDataItem& item) |
353 | { |
354 | ASSERT(m_stream); |
355 | |
356 | if (m_fileOpened) { |
357 | m_stream->read(m_buffer.data(), m_buffer.size()); |
358 | return; |
359 | } |
360 | |
361 | long long bytesToRead = m_itemLengthList[m_readItemCount] - m_currentItemReadSize; |
362 | if (bytesToRead > m_totalRemainingSize) |
363 | bytesToRead = static_cast<int>(m_totalRemainingSize); |
364 | m_stream->openForRead(item.file()->path(), item.offset() + m_currentItemReadSize, bytesToRead); |
365 | m_fileOpened = true; |
366 | m_currentItemReadSize = 0; |
367 | } |
368 | |
369 | void NetworkDataTaskBlob::didOpen(bool success) |
370 | { |
371 | if (m_state == State::Canceling || m_state == State::Completed || (!m_client && !isDownload())) { |
372 | clearStream(); |
373 | return; |
374 | } |
375 | |
376 | if (!success) { |
377 | didFail(Error::NotReadableError); |
378 | return; |
379 | } |
380 | |
381 | Ref<NetworkDataTaskBlob> protectedThis(*this); |
382 | read(); |
383 | } |
384 | |
385 | void NetworkDataTaskBlob::didRead(int bytesRead) |
386 | { |
387 | if (m_state == State::Canceling || m_state == State::Completed || (!m_client && !isDownload())) { |
388 | clearStream(); |
389 | return; |
390 | } |
391 | |
392 | if (bytesRead < 0) { |
393 | didFail(Error::NotReadableError); |
394 | return; |
395 | } |
396 | |
397 | Ref<NetworkDataTaskBlob> protectedThis(*this); |
398 | consumeData(m_buffer.data(), bytesRead); |
399 | } |
400 | |
401 | void NetworkDataTaskBlob::consumeData(const char* data, int bytesRead) |
402 | { |
403 | m_totalRemainingSize -= bytesRead; |
404 | |
405 | if (bytesRead) { |
406 | if (m_downloadFile != FileSystem::invalidPlatformFileHandle) { |
407 | if (!writeDownload(data, bytesRead)) |
408 | return; |
409 | } else { |
410 | ASSERT(m_client); |
411 | m_client->didReceiveData(SharedBuffer::create(data, bytesRead)); |
412 | } |
413 | } |
414 | |
415 | if (m_fileOpened) { |
416 | // When the current item is a file item, the reading is completed only if bytesRead is 0. |
417 | if (!bytesRead) { |
418 | // Close the file. |
419 | m_fileOpened = false; |
420 | m_stream->close(); |
421 | |
422 | // Move to the next item. |
423 | m_readItemCount++; |
424 | } |
425 | } else { |
426 | // Otherwise, we read the current text item as a whole and move to the next item. |
427 | m_readItemCount++; |
428 | } |
429 | |
430 | read(); |
431 | } |
432 | |
433 | void NetworkDataTaskBlob::setPendingDownloadLocation(const String& filename, SandboxExtension::Handle&& sandboxExtensionHandle, bool allowOverwrite) |
434 | { |
435 | NetworkDataTask::setPendingDownloadLocation(filename, { }, allowOverwrite); |
436 | |
437 | ASSERT(!m_sandboxExtension); |
438 | m_sandboxExtension = SandboxExtension::create(WTFMove(sandboxExtensionHandle)); |
439 | if (m_sandboxExtension) |
440 | m_sandboxExtension->consume(); |
441 | |
442 | if (allowOverwrite && FileSystem::fileExists(m_pendingDownloadLocation)) |
443 | FileSystem::deleteFile(m_pendingDownloadLocation); |
444 | } |
445 | |
446 | String NetworkDataTaskBlob::suggestedFilename() const |
447 | { |
448 | if (!m_suggestedFilename.isEmpty()) |
449 | return m_suggestedFilename; |
450 | |
451 | return "unknown"_s ; |
452 | } |
453 | |
454 | void NetworkDataTaskBlob::download() |
455 | { |
456 | ASSERT(isDownload()); |
457 | ASSERT(m_pendingDownloadLocation); |
458 | |
459 | LOG(NetworkSession, "%p - NetworkDataTaskBlob::download to %s" , this, m_pendingDownloadLocation.utf8().data()); |
460 | |
461 | m_downloadFile = FileSystem::openFile(m_pendingDownloadLocation, FileSystem::FileOpenMode::Write); |
462 | if (m_downloadFile == FileSystem::invalidPlatformFileHandle) { |
463 | didFailDownload(cancelledError(m_firstRequest)); |
464 | return; |
465 | } |
466 | |
467 | auto& downloadManager = m_networkProcess->downloadManager(); |
468 | auto download = std::make_unique<Download>(downloadManager, m_pendingDownloadID, *this, m_session->sessionID(), suggestedFilename()); |
469 | auto* downloadPtr = download.get(); |
470 | downloadManager.dataTaskBecameDownloadTask(m_pendingDownloadID, WTFMove(download)); |
471 | downloadPtr->didCreateDestination(m_pendingDownloadLocation); |
472 | |
473 | ASSERT(!m_client); |
474 | |
475 | m_buffer.resize(bufferSize); |
476 | read(); |
477 | } |
478 | |
479 | bool NetworkDataTaskBlob::writeDownload(const char* data, int bytesRead) |
480 | { |
481 | ASSERT(isDownload()); |
482 | int bytesWritten = FileSystem::writeToFile(m_downloadFile, data, bytesRead); |
483 | if (bytesWritten == -1) { |
484 | didFailDownload(cancelledError(m_firstRequest)); |
485 | return false; |
486 | } |
487 | |
488 | ASSERT(bytesWritten == bytesRead); |
489 | auto* download = m_networkProcess->downloadManager().download(m_pendingDownloadID); |
490 | ASSERT(download); |
491 | download->didReceiveData(bytesWritten); |
492 | return true; |
493 | } |
494 | |
495 | void NetworkDataTaskBlob::cleanDownloadFiles() |
496 | { |
497 | if (m_downloadFile != FileSystem::invalidPlatformFileHandle) { |
498 | FileSystem::closeFile(m_downloadFile); |
499 | m_downloadFile = FileSystem::invalidPlatformFileHandle; |
500 | } |
501 | FileSystem::deleteFile(m_pendingDownloadLocation); |
502 | } |
503 | |
504 | void NetworkDataTaskBlob::didFailDownload(const ResourceError& error) |
505 | { |
506 | LOG(NetworkSession, "%p - NetworkDataTaskBlob::didFailDownload" , this); |
507 | |
508 | clearStream(); |
509 | cleanDownloadFiles(); |
510 | |
511 | if (m_sandboxExtension) { |
512 | m_sandboxExtension->revoke(); |
513 | m_sandboxExtension = nullptr; |
514 | } |
515 | |
516 | if (m_client) |
517 | m_client->didCompleteWithError(error); |
518 | else { |
519 | auto* download = m_networkProcess->downloadManager().download(m_pendingDownloadID); |
520 | ASSERT(download); |
521 | download->didFail(error, IPC::DataReference()); |
522 | } |
523 | } |
524 | |
525 | void NetworkDataTaskBlob::didFinishDownload() |
526 | { |
527 | LOG(NetworkSession, "%p - NetworkDataTaskBlob::didFinishDownload" , this); |
528 | |
529 | ASSERT(isDownload()); |
530 | FileSystem::closeFile(m_downloadFile); |
531 | m_downloadFile = FileSystem::invalidPlatformFileHandle; |
532 | |
533 | if (m_sandboxExtension) { |
534 | m_sandboxExtension->revoke(); |
535 | m_sandboxExtension = nullptr; |
536 | } |
537 | |
538 | clearStream(); |
539 | auto* download = m_networkProcess->downloadManager().download(m_pendingDownloadID); |
540 | ASSERT(download); |
541 | download->didFinish(); |
542 | } |
543 | |
544 | void NetworkDataTaskBlob::didFail(Error errorCode) |
545 | { |
546 | ASSERT(!m_sandboxExtension); |
547 | |
548 | Ref<NetworkDataTaskBlob> protectedThis(*this); |
549 | if (isDownload()) { |
550 | didFailDownload(ResourceError(webKitBlobResourceDomain, static_cast<int>(errorCode), m_firstRequest.url(), String())); |
551 | return; |
552 | } |
553 | |
554 | LOG(NetworkSession, "%p - NetworkDataTaskBlob::didFail" , this); |
555 | |
556 | clearStream(); |
557 | ASSERT(m_client); |
558 | m_client->didCompleteWithError(ResourceError(webKitBlobResourceDomain, static_cast<int>(errorCode), m_firstRequest.url(), String())); |
559 | } |
560 | |
561 | void NetworkDataTaskBlob::didFinish() |
562 | { |
563 | if (m_downloadFile != FileSystem::invalidPlatformFileHandle) { |
564 | didFinishDownload(); |
565 | return; |
566 | } |
567 | |
568 | ASSERT(!m_sandboxExtension); |
569 | |
570 | LOG(NetworkSession, "%p - NetworkDataTaskBlob::didFinish" , this); |
571 | |
572 | clearStream(); |
573 | ASSERT(m_client); |
574 | m_client->didCompleteWithError({ }); |
575 | } |
576 | |
577 | } // namespace WebKit |
578 | |