Bug 1036275 - Add Packaged App Service r=honzab
authorValentin Gosu <valentin.gosu@gmail.com>
Wed, 03 Jun 2015 01:46:15 +0300
changeset 277635 b62f0a6064182751ce465db4444befb4394e10e7
parent 277634 c2939bd290fabbfa7d64af7c2b392bb3cc399310
child 277636 b38c7d19b1d63a084ba487487fba26c0e963f362
push id4932
push userjlund@mozilla.com
push dateMon, 10 Aug 2015 18:23:06 +0000
treeherdermozilla-beta@6dd5a4f5f745 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewershonzab
bugs1036275
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1036275 - Add Packaged App Service r=honzab
netwerk/base/moz.build
netwerk/base/nsIPackagedAppService.idl
netwerk/build/nsNetCID.h
netwerk/build/nsNetModule.cpp
netwerk/protocol/http/PackagedAppService.cpp
netwerk/protocol/http/PackagedAppService.h
netwerk/protocol/http/moz.build
netwerk/test/unit/test_packaged_app_service.js
netwerk/test/unit/xpcshell.ini
--- a/netwerk/base/moz.build
+++ b/netwerk/base/moz.build
@@ -62,16 +62,17 @@ XPIDL_SOURCES += [
     'nsINetworkInterceptController.idl',
     'nsINetworkLinkService.idl',
     'nsINetworkPredictor.idl',
     'nsINetworkPredictorVerifier.idl',
     'nsINetworkProperties.idl',
     'nsINSSErrorsService.idl',
     'nsINullChannel.idl',
     'nsIPACGenerator.idl',
+    'nsIPackagedAppService.idl',
     'nsIParentChannel.idl',
     'nsIParentRedirectingChannel.idl',
     'nsIPermission.idl',
     'nsIPermissionManager.idl',
     'nsIPrivateBrowsingChannel.idl',
     'nsIProgressEventSink.idl',
     'nsIPrompt.idl',
     'nsIProtocolHandler.idl',
new file mode 100644
--- /dev/null
+++ b/netwerk/base/nsIPackagedAppService.idl
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+interface nsILoadContextInfo;
+interface nsICacheEntryOpenCallback;
+
+%{C++
+  #define PACKAGED_APP_TOKEN "!//"
+%}
+
+/**
+ * nsIPackagedAppService
+ */
+[scriptable, builtinclass, uuid(77f9a34d-d082-43f1-9f83-e852d0173cd5)]
+interface nsIPackagedAppService : nsISupports
+{
+  /**
+   * @aURI is a URL to a packaged resource
+   *   - format:  package_url + PACKAGED_APP_TOKEN + resource_path
+   *   - example: http://test.com/path/to/package!//resource.html
+   * @aCallback is an object implementing nsICacheEntryOpenCallback
+   *   - this is the target of the async result of the operation
+   *   - aCallback->OnCacheEntryCheck() is called to verify the entry is valid
+   *   - aCallback->OnCacheEntryAvailable() is called with a pointer to the
+   *     the cached entry, if one exists, or an error code otherwise
+   *   - aCallback is kept alive using an nsCOMPtr until OnCacheEntryAvailable
+   *     is called
+   * @aInfo is an object used to determine the cache jar this resource goes in.
+   *   - usually created by calling GetLoadContextInfo(requestingChannel)
+   *
+   * Calling this method will either download the package containing the given
+   * resource URI, store it in the cache and pass the cache entry to aCallback,
+   * or if that resource has already been downloaded it will be served from
+   * the cache.
+   */
+  void requestURI(in nsIURI aURI, in nsILoadContextInfo aInfo, in nsICacheEntryOpenCallback aCallback);
+};
--- a/netwerk/build/nsNetCID.h
+++ b/netwerk/build/nsNetCID.h
@@ -863,16 +863,26 @@
 #define NS_DASHBOARD_CID                               \
 {   /*c79eb3c6-091a-45a6-8544-5a8d1ab79537 */          \
     0xc79eb3c6,                                        \
     0x091a,                                            \
     0x45a6,                                            \
     { 0x85, 0x44, 0x5a, 0x8d, 0x1a, 0xb7, 0x95, 0x37 } \
 }
 
+#define NS_PACKAGEDAPPSERVICE_CONTRACTID \
+    "@mozilla.org/network/packaged-app-service;1"
+#define NS_PACKAGEDAPPSERVICE_CID                      \
+{   /* adef6762-41b9-4470-a06a-dc29cf8de381 */         \
+    0xadef6762,                                        \
+    0x41b9,                                            \
+    0x4470,                                            \
+  { 0xa0, 0x6a, 0xdc, 0x29, 0xcf, 0x8d, 0xe3, 0x81 }   \
+}
+
 
 /******************************************************************************
  * netwerk/cookie classes
  */
 
 // service implementing nsICookieManager and nsICookieManager2.
 #define NS_COOKIEMANAGER_CONTRACTID \
     "@mozilla.org/cookiemanager;1"
--- a/netwerk/build/nsNetModule.cpp
+++ b/netwerk/build/nsNetModule.cpp
@@ -246,19 +246,21 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsHttpCha
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsHttpActivityDistributor)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsHttpBasicAuth)
 NS_GENERIC_FACTORY_CONSTRUCTOR(nsHttpDigestAuth)
 }
 }
 #endif // !NECKO_PROTOCOL_http
 
 #include "mozilla/net/Dashboard.h"
+#include "mozilla/net/PackagedAppService.h"
 namespace mozilla {
 namespace net {
   NS_GENERIC_FACTORY_CONSTRUCTOR(Dashboard)
+  NS_GENERIC_FACTORY_CONSTRUCTOR(PackagedAppService)
 }
 }
 #include "AppProtocolHandler.h"
 
 #ifdef NECKO_PROTOCOL_res
 // resource
 #include "nsResProtocolHandler.h"
 NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsResProtocolHandler, Init)
@@ -704,16 +706,17 @@ NS_DEFINE_NAMED_CID(NS_NOAUTHURLPARSER_C
 NS_DEFINE_NAMED_CID(NS_AUTHURLPARSER_CID);
 NS_DEFINE_NAMED_CID(NS_STANDARDURL_CID);
 NS_DEFINE_NAMED_CID(NS_ARRAYBUFFERINPUTSTREAM_CID);
 NS_DEFINE_NAMED_CID(NS_BUFFEREDINPUTSTREAM_CID);
 NS_DEFINE_NAMED_CID(NS_BUFFEREDOUTPUTSTREAM_CID);
 NS_DEFINE_NAMED_CID(NS_MIMEINPUTSTREAM_CID);
 NS_DEFINE_NAMED_CID(NS_PROTOCOLPROXYSERVICE_CID);
 NS_DEFINE_NAMED_CID(NS_STREAMCONVERTERSERVICE_CID);
+NS_DEFINE_NAMED_CID(NS_PACKAGEDAPPSERVICE_CID);
 NS_DEFINE_NAMED_CID(NS_DASHBOARD_CID);
 #ifdef NECKO_PROTOCOL_ftp
 NS_DEFINE_NAMED_CID(NS_FTPDIRLISTINGCONVERTER_CID);
 #endif
 NS_DEFINE_NAMED_CID(NS_NSINDEXEDTOHTMLCONVERTER_CID);
 NS_DEFINE_NAMED_CID(NS_DIRINDEXPARSER_CID);
 NS_DEFINE_NAMED_CID(NS_MULTIMIXEDCONVERTER_CID);
 NS_DEFINE_NAMED_CID(NS_UNKNOWNDECODER_CID);
@@ -848,16 +851,17 @@ static const mozilla::Module::CIDEntry k
     { &kNS_AUTHURLPARSER_CID, false, nullptr, nsAuthURLParserConstructor },
     { &kNS_STANDARDURL_CID, false, nullptr, nsStandardURLConstructor },
     { &kNS_ARRAYBUFFERINPUTSTREAM_CID, false, nullptr, ArrayBufferInputStreamConstructor },
     { &kNS_BUFFEREDINPUTSTREAM_CID, false, nullptr, nsBufferedInputStream::Create },
     { &kNS_BUFFEREDOUTPUTSTREAM_CID, false, nullptr, nsBufferedOutputStream::Create },
     { &kNS_MIMEINPUTSTREAM_CID, false, nullptr, nsMIMEInputStreamConstructor },
     { &kNS_PROTOCOLPROXYSERVICE_CID, true, nullptr, nsProtocolProxyServiceConstructor },
     { &kNS_STREAMCONVERTERSERVICE_CID, false, nullptr, CreateNewStreamConvServiceFactory },
+    { &kNS_PACKAGEDAPPSERVICE_CID, false, NULL, mozilla::net::PackagedAppServiceConstructor },
     { &kNS_DASHBOARD_CID, false, nullptr, mozilla::net::DashboardConstructor },
 #ifdef NECKO_PROTOCOL_ftp
     { &kNS_FTPDIRLISTINGCONVERTER_CID, false, nullptr, CreateNewFTPDirListingConv },
 #endif
     { &kNS_NSINDEXEDTOHTMLCONVERTER_CID, false, nullptr, nsIndexedToHTML::Create },
     { &kNS_DIRINDEXPARSER_CID, false, nullptr, nsDirIndexParserConstructor },
     { &kNS_MULTIMIXEDCONVERTER_CID, false, nullptr, CreateNewMultiMixedConvFactory },
     { &kNS_UNKNOWNDECODER_CID, false, nullptr, CreateNewUnknownDecoderFactory },
@@ -994,16 +998,17 @@ static const mozilla::Module::ContractID
     { NS_AUTHURLPARSER_CONTRACTID, &kNS_AUTHURLPARSER_CID },
     { NS_STANDARDURL_CONTRACTID, &kNS_STANDARDURL_CID },
     { NS_ARRAYBUFFERINPUTSTREAM_CONTRACTID, &kNS_ARRAYBUFFERINPUTSTREAM_CID },
     { NS_BUFFEREDINPUTSTREAM_CONTRACTID, &kNS_BUFFEREDINPUTSTREAM_CID },
     { NS_BUFFEREDOUTPUTSTREAM_CONTRACTID, &kNS_BUFFEREDOUTPUTSTREAM_CID },
     { NS_MIMEINPUTSTREAM_CONTRACTID, &kNS_MIMEINPUTSTREAM_CID },
     { NS_PROTOCOLPROXYSERVICE_CONTRACTID, &kNS_PROTOCOLPROXYSERVICE_CID },
     { NS_STREAMCONVERTERSERVICE_CONTRACTID, &kNS_STREAMCONVERTERSERVICE_CID },
+    { NS_PACKAGEDAPPSERVICE_CONTRACTID, &kNS_PACKAGEDAPPSERVICE_CID },
     { NS_DASHBOARD_CONTRACTID, &kNS_DASHBOARD_CID },
 #ifdef NECKO_PROTOCOL_ftp
     { NS_ISTREAMCONVERTER_KEY FTP_TO_INDEX, &kNS_FTPDIRLISTINGCONVERTER_CID },
 #endif
     { NS_ISTREAMCONVERTER_KEY INDEX_TO_HTML, &kNS_NSINDEXEDTOHTMLCONVERTER_CID },
     { NS_DIRINDEXPARSER_CONTRACTID, &kNS_DIRINDEXPARSER_CID },
     { NS_ISTREAMCONVERTER_KEY MULTI_MIXED_X, &kNS_MULTIMIXEDCONVERTER_CID },
     { NS_ISTREAMCONVERTER_KEY MULTI_BYTERANGES, &kNS_MULTIMIXEDCONVERTER_CID },
new file mode 100644
--- /dev/null
+++ b/netwerk/protocol/http/PackagedAppService.cpp
@@ -0,0 +1,554 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "PackagedAppService.h"
+#include "nsICacheStorage.h"
+#include "LoadContextInfo.h"
+#include "nsICacheStorageService.h"
+#include "nsIResponseHeadProvider.h"
+#include "nsIMultiPartChannel.h"
+#include "../../cache2/CacheFileUtils.h"
+#include "nsStreamUtils.h"
+
+namespace mozilla {
+namespace net {
+
+static PackagedAppService *gPackagedAppService = nullptr;
+
+NS_IMPL_ISUPPORTS(PackagedAppService, nsIPackagedAppService)
+
+NS_IMPL_ISUPPORTS(PackagedAppService::CacheEntryWriter, nsIStreamListener)
+
+/* static */ nsresult
+PackagedAppService::CacheEntryWriter::Create(nsIURI *aURI,
+                                             nsICacheStorage *aStorage,
+                                             CacheEntryWriter **aResult)
+{
+  nsRefPtr<CacheEntryWriter> writer = new CacheEntryWriter();
+  nsresult rv = aStorage->OpenTruncate(aURI, EmptyCString(),
+                                       getter_AddRefs(writer->mEntry));
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  rv = writer->mEntry->ForceValidFor(PR_UINT32_MAX);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  writer.forget(aResult);
+  return NS_OK;
+}
+
+NS_METHOD
+PackagedAppService::CacheEntryWriter::ConsumeData(nsIInputStream *aStream,
+                                                  void *aClosure,
+                                                  const char *aFromRawSegment,
+                                                  uint32_t aToOffset,
+                                                  uint32_t aCount,
+                                                  uint32_t *aWriteCount)
+{
+  MOZ_ASSERT(aClosure, "The closure must not be null");
+  CacheEntryWriter *self = static_cast<CacheEntryWriter*>(aClosure);
+  MOZ_ASSERT(self->mOutputStream, "The stream should not be null");
+  return self->mOutputStream->Write(aFromRawSegment, aCount, aWriteCount);
+}
+
+NS_IMETHODIMP
+PackagedAppService::CacheEntryWriter::OnStartRequest(nsIRequest *aRequest,
+                                                     nsISupports *aContext)
+{
+  nsCOMPtr<nsIResponseHeadProvider> provider(do_QueryInterface(aRequest));
+  if (!provider) {
+    return NS_ERROR_INVALID_ARG;
+  }
+  nsHttpResponseHead *responseHead = provider->GetResponseHead();
+  if (!responseHead) {
+    return NS_ERROR_FAILURE;
+  }
+
+  mEntry->SetPredictedDataSize(responseHead->TotalEntitySize());
+
+  nsAutoCString head;
+  responseHead->Flatten(head, true);
+  nsresult rv = mEntry->SetMetaDataElement("response-head", head.get());
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = mEntry->SetMetaDataElement("request-method", "GET");
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  rv = mEntry->OpenOutputStream(0, getter_AddRefs(mOutputStream));
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PackagedAppService::CacheEntryWriter::OnStopRequest(nsIRequest *aRequest,
+                                                    nsISupports *aContext,
+                                                    nsresult aStatusCode)
+{
+  if (mOutputStream) {
+    mOutputStream->Close();
+    mOutputStream = nullptr;
+  }
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PackagedAppService::CacheEntryWriter::OnDataAvailable(nsIRequest *aRequest,
+                                                      nsISupports *aContext,
+                                                      nsIInputStream *aInputStream,
+                                                      uint64_t aOffset,
+                                                      uint32_t aCount)
+{
+  if (!aInputStream) {
+    return NS_ERROR_INVALID_ARG;
+  }
+  // Calls ConsumeData to read the data into the cache entry
+  uint32_t n;
+  return aInputStream->ReadSegments(ConsumeData, this, aCount, &n);
+}
+
+
+NS_IMPL_ISUPPORTS(PackagedAppService::PackagedAppDownloader, nsIStreamListener)
+
+nsresult
+PackagedAppService::PackagedAppDownloader::Init(nsILoadContextInfo* aInfo,
+                                                const nsCString& aKey)
+{
+  nsresult rv;
+  nsCOMPtr<nsICacheStorageService> cacheStorageService =
+    do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  rv = cacheStorageService->DiskCacheStorage(aInfo, false,
+                                             getter_AddRefs(mCacheStorage));
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  mPackageKey = aKey;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PackagedAppService::PackagedAppDownloader::OnStartRequest(nsIRequest *aRequest,
+                                                          nsISupports *aContext)
+{
+  // In case an error occurs in this method mWriter should be null
+  // so we don't accidentally write to the previous resource's cache entry.
+  mWriter = nullptr;
+
+  nsCOMPtr<nsIURI> uri;
+  nsresult rv = GetSubresourceURI(aRequest, getter_AddRefs(uri));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return NS_OK;
+  }
+
+  rv = CacheEntryWriter::Create(uri, mCacheStorage, getter_AddRefs(mWriter));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return NS_OK;
+  }
+
+  MOZ_ASSERT(mWriter);
+  rv = mWriter->OnStartRequest(aRequest, aContext);
+  NS_WARN_IF(NS_FAILED(rv));
+  return NS_OK;
+}
+
+nsresult
+PackagedAppService::PackagedAppDownloader::GetSubresourceURI(nsIRequest * aRequest,
+                                                             nsIURI ** aResult)
+{
+  nsresult rv;
+  nsCOMPtr<nsIResponseHeadProvider> provider(do_QueryInterface(aRequest));
+  nsCOMPtr<nsIChannel> chan(do_QueryInterface(aRequest));
+
+  if (NS_WARN_IF(!provider || !chan)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  nsHttpResponseHead *responseHead = provider->GetResponseHead();
+  if (NS_WARN_IF(!responseHead)) {
+    return NS_ERROR_FAILURE;
+  }
+  nsAutoCString contentLocation;
+  rv = responseHead->GetHeader(nsHttp::ResolveAtom("Content-Location"), contentLocation);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  nsCOMPtr<nsIURI> uri;
+  rv = chan->GetURI(getter_AddRefs(uri));
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  nsAutoCString path;
+  rv = uri->GetPath(path);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  path += PACKAGED_APP_TOKEN;
+
+  // TODO: make sure the path is normalized
+  if (StringBeginsWith(contentLocation, NS_LITERAL_CSTRING("/"))) {
+    contentLocation = Substring(contentLocation, 1);
+  }
+
+  path += contentLocation;
+
+  nsCOMPtr<nsIURI> partURI;
+  rv = uri->CloneIgnoringRef(getter_AddRefs(partURI));
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  rv = partURI->SetPath(path);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  partURI.forget(aResult);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PackagedAppService::PackagedAppDownloader::OnStopRequest(nsIRequest *aRequest,
+                                                         nsISupports *aContext,
+                                                         nsresult aStatusCode)
+{
+  nsCOMPtr<nsIMultiPartChannel> multiChannel(do_QueryInterface(aRequest));
+  nsresult rv;
+
+
+  // The request is normally a multiPartChannel. If it isn't, it generally means
+  // an error has occurred in nsMultiMixedConv.
+  // If an error occurred in OnStartRequest, mWriter could be null.
+  if (multiChannel && mWriter) {
+    mWriter->OnStopRequest(aRequest, aContext, aStatusCode);
+
+    nsCOMPtr<nsIURI> uri;
+    rv = GetSubresourceURI(aRequest, getter_AddRefs(uri));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return NS_OK;
+    }
+
+    nsCOMPtr<nsICacheEntry> entry;
+    mWriter->mEntry.swap(entry);
+
+    // We don't need the writer anymore - this will close its stream
+    mWriter = nullptr;
+    CallCallbacks(uri, entry, aStatusCode);
+  }
+
+  bool lastPart = false;
+  if (multiChannel) {
+    rv = multiChannel->GetIsLastPart(&lastPart);
+    if (NS_SUCCEEDED(rv) && !lastPart) {
+      // If this isn't the last part, we don't do the cleanup yet
+      return NS_OK;
+    }
+  }
+
+  // If this is the last part of the package, it means the requested resources
+  // have not been found in the package so we return an appropriate error.
+  if (NS_SUCCEEDED(aStatusCode) && lastPart) {
+    aStatusCode = NS_ERROR_FILE_NOT_FOUND;
+  }
+
+  nsRefPtr<PackagedAppDownloader> kungFuDeathGrip(this);
+  // NotifyPackageDownloaded removes the ref from the array. Keep a temp ref
+  if (gPackagedAppService) {
+    gPackagedAppService->NotifyPackageDownloaded(mPackageKey);
+  }
+  ClearCallbacks(aStatusCode);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PackagedAppService::PackagedAppDownloader::OnDataAvailable(nsIRequest *aRequest,
+                                                           nsISupports *aContext,
+                                                           nsIInputStream *aInputStream,
+                                                           uint64_t aOffset,
+                                                           uint32_t aCount)
+{
+  if (!mWriter) {
+    uint32_t n;
+    return aInputStream->ReadSegments(NS_DiscardSegment, nullptr, aCount, &n);
+  }
+  return mWriter->OnDataAvailable(aRequest, aContext, aInputStream, aOffset,
+                                  aCount);
+}
+
+nsresult
+PackagedAppService::PackagedAppDownloader::AddCallback(nsIURI *aURI,
+                                                       nsICacheEntryOpenCallback *aCallback)
+{
+  MOZ_RELEASE_ASSERT(NS_IsMainThread(), "mCallbacks hashtable is not thread safe");
+  nsAutoCString spec;
+  aURI->GetAsciiSpec(spec);
+
+  // Check if we already have a resource waiting for this resource
+  nsCOMArray<nsICacheEntryOpenCallback>* array = mCallbacks.Get(spec);
+  if (array) {
+    // Add this resource to the callback array
+    array->AppendObject(aCallback);
+  } else {
+    // This is the first callback for this URI.
+    // Create a new array and add the callback
+    nsCOMArray<nsICacheEntryOpenCallback>* newArray =
+      new nsCOMArray<nsICacheEntryOpenCallback>();
+    newArray->AppendObject(aCallback);
+    mCallbacks.Put(spec, newArray);
+  }
+  return NS_OK;
+}
+
+nsresult
+PackagedAppService::PackagedAppDownloader::CallCallbacks(nsIURI *aURI,
+                                                         nsICacheEntry *aEntry,
+                                                         nsresult aResult)
+{
+  MOZ_RELEASE_ASSERT(NS_IsMainThread(), "mCallbacks hashtable is not thread safe");
+  // Hold on to this entry while calling the callbacks
+  nsCOMPtr<nsICacheEntry> handle(aEntry);
+
+  nsAutoCString spec;
+  aURI->GetSpec(spec);
+
+  nsCOMArray<nsICacheEntryOpenCallback>* array = mCallbacks.Get(spec);
+  if (array) {
+    // Call all the callbacks for this URI
+    for (uint32_t i = 0; i < array->Length(); ++i) {
+      nsCOMPtr<nsICacheEntryOpenCallback> callback(array->ObjectAt(i));
+      // We call to AsyncOpenURI which automatically calls the callback.
+      mCacheStorage->AsyncOpenURI(aURI, EmptyCString(),
+                                  nsICacheStorage::OPEN_READONLY, callback);
+    }
+    // Clear the array and remove it from the hashtable
+    array->Clear();
+    mCallbacks.Remove(spec);
+    aEntry->ForceValidFor(0);
+  }
+  return NS_OK;
+}
+
+PLDHashOperator
+PackagedAppService::PackagedAppDownloader::ClearCallbacksEnumerator(const nsACString& key,
+  nsAutoPtr<nsCOMArray<nsICacheEntryOpenCallback> >& callbackArray,
+  void* arg)
+{
+  MOZ_ASSERT(arg, "The void* parameter should be a pointer to nsresult");
+  nsresult *result = static_cast<nsresult*>(arg);
+  for (uint32_t i = 0; i < callbackArray->Length(); ++i) {
+    nsCOMPtr<nsICacheEntryOpenCallback> callback = callbackArray->ObjectAt(i);
+    callback->OnCacheEntryAvailable(nullptr, false, nullptr, *result);
+  }
+  // Remove entry from hashtable
+  return PL_DHASH_REMOVE;
+}
+
+nsresult
+PackagedAppService::PackagedAppDownloader::ClearCallbacks(nsresult aResult)
+{
+  MOZ_RELEASE_ASSERT(NS_IsMainThread(), "mCallbacks hashtable is not thread safe");
+  mCallbacks.Enumerate(ClearCallbacksEnumerator, &aResult);
+  return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(PackagedAppService::CacheEntryChecker, nsICacheEntryOpenCallback)
+
+NS_IMETHODIMP
+PackagedAppService::CacheEntryChecker::OnCacheEntryCheck(nsICacheEntry *aEntry,
+                                                         nsIApplicationCache *aApplicationCache,
+                                                         uint32_t *_retval)
+{
+  return mCallback->OnCacheEntryCheck(aEntry, aApplicationCache, _retval);
+}
+
+NS_IMETHODIMP
+PackagedAppService::CacheEntryChecker::OnCacheEntryAvailable(nsICacheEntry *aEntry,
+                                                             bool aNew,
+                                                             nsIApplicationCache *aApplicationCache,
+                                                             nsresult aResult)
+{
+  if (aResult == NS_ERROR_CACHE_KEY_NOT_FOUND) {
+    MOZ_ASSERT(!aEntry, "No entry");
+    // trigger download
+    // download checks if package download is already in progress
+    gPackagedAppService->OpenNewPackageInternal(mURI, mCallback,
+                                                mLoadContextInfo);
+  } else {
+    // TODO: if aResult is another error code, should we pass it off to the
+    // consumer, or should we try to download the package again?
+    mCallback->OnCacheEntryAvailable(aEntry, aNew, aApplicationCache, aResult);
+    // TODO: update last access entry for the entire package
+  }
+  return NS_OK;
+}
+
+PackagedAppService::PackagedAppService()
+{
+  gPackagedAppService = this;
+}
+
+PackagedAppService::~PackagedAppService()
+{
+  gPackagedAppService = nullptr;
+}
+
+NS_IMETHODIMP
+PackagedAppService::RequestURI(nsIURI *aURI,
+                               nsILoadContextInfo *aInfo,
+                               nsICacheEntryOpenCallback *aCallback)
+{
+  // Check arguments are not null
+  if (!aURI || !aCallback || !aInfo) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  nsAutoCString path;
+  aURI->GetPath(path);
+  int32_t pos = path.Find(PACKAGED_APP_TOKEN);
+  if (pos == kNotFound) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  nsresult rv;
+  nsCOMPtr<nsICacheStorageService> cacheStorageService =
+    do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv);
+
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<nsICacheStorage> cacheStorage;
+
+  rv = cacheStorageService->DiskCacheStorage(aInfo, false,
+                                             getter_AddRefs(cacheStorage));
+
+  nsRefPtr<CacheEntryChecker> checker = new CacheEntryChecker(aURI, aCallback, aInfo);
+  return cacheStorage->AsyncOpenURI(aURI, EmptyCString(),
+                                    nsICacheStorage::OPEN_READONLY, checker);
+}
+
+nsresult
+PackagedAppService::NotifyPackageDownloaded(nsCString aKey)
+{
+  MOZ_RELEASE_ASSERT(NS_IsMainThread(), "mDownloadingPackages hashtable is not thread safe");
+  mDownloadingPackages.Remove(aKey);
+  return NS_OK;
+}
+
+nsresult
+PackagedAppService::OpenNewPackageInternal(nsIURI *aURI,
+                                           nsICacheEntryOpenCallback *aCallback,
+                                           nsILoadContextInfo *aInfo)
+{
+  MOZ_RELEASE_ASSERT(NS_IsMainThread(), "mDownloadingPackages hashtable is not thread safe");
+
+  nsAutoCString path;
+  nsresult rv = aURI->GetPath(path);
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  int32_t pos = path.Find(PACKAGED_APP_TOKEN);
+  MOZ_ASSERT(pos != kNotFound,
+             "This should never be called if the token is missing");
+
+  nsCOMPtr<nsIURI> packageURI;
+  rv = aURI->CloneIgnoringRef(getter_AddRefs(packageURI));
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  rv = packageURI->SetPath(Substring(path, 0, pos));
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  nsAutoCString key;
+  CacheFileUtils::AppendKeyPrefix(aInfo, key);
+
+  {
+    nsAutoCString spec;
+    packageURI->GetAsciiSpec(spec);
+    key += ":";
+    key += spec;
+  }
+
+  nsRefPtr<PackagedAppDownloader> downloader;
+  if (mDownloadingPackages.Get(key, getter_AddRefs(downloader))) {
+    // We have determined that the file is not in the cache.
+    // If we find that the package that the file belongs to is currently being
+    // downloaded, we will add the callback to the package's queue, and it will
+    // be called once the file is processed and saved in the cache.
+
+    downloader->AddCallback(aURI, aCallback);
+    return NS_OK;
+  }
+
+  nsCOMPtr<nsIChannel> channel;
+  rv = NS_NewChannel(
+    getter_AddRefs(channel), packageURI, nsContentUtils::GetSystemPrincipal(),
+    nsILoadInfo::SEC_NORMAL, nsIContentPolicy::TYPE_OTHER, nullptr, nullptr,
+    nsIRequest::LOAD_NORMAL);
+
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<nsICachingChannel> cacheChan(do_QueryInterface(channel));
+  if (cacheChan) {
+    // Each resource in the package will be put in its own cache entry
+    // during the first load of the package, so we only want the channel to
+    // cache the response head, not the entire content of the package.
+    cacheChan->SetCacheOnlyMetadata(true);
+  }
+
+  downloader = new PackagedAppDownloader();
+  rv = downloader->Init(aInfo, key);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  downloader->AddCallback(aURI, aCallback);
+
+  nsCOMPtr<nsIStreamConverterService> streamconv =
+    do_GetService("@mozilla.org/streamConverters;1", &rv);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  nsCOMPtr<nsIStreamListener> mimeConverter;
+  rv = streamconv->AsyncConvertData("multipart/mixed", "*/*", downloader, nullptr,
+                                    getter_AddRefs(mimeConverter));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
+  // Add the package to the hashtable.
+  mDownloadingPackages.Put(key, downloader);
+
+  return channel->AsyncOpen(mimeConverter, nullptr);
+}
+
+} // namespace net
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/netwerk/protocol/http/PackagedAppService.h
@@ -0,0 +1,191 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_PackagedAppService_h
+#define mozilla_net_PackagedAppService_h
+
+#include "nsIPackagedAppService.h"
+#include "nsILoadContextInfo.h"
+#include "nsICacheStorage.h"
+
+namespace mozilla {
+namespace net {
+
+// This service is used to download packages from the web.
+// Individual resources in the package are saved in the browser cache. It also
+// provides an interface to asynchronously request resources from packages,
+// which are either returned from the cache if they exist and are valid,
+// or downloads the package.
+// The package format is defined at:
+//     https://w3ctag.github.io/packaging-on-the-web/#streamable-package-format
+// Downloading the package is triggered by calling requestURI(aURI, aInfo, aCallback)
+//     aURI is the subresource uri - http://domain.com/path/package!//resource.html
+//     aInfo is a nsILoadContextInfo used to pick the cache jar the resource goes into
+//     aCallback is the target of the async call to requestURI
+// When requestURI is called, a CacheEntryChecker is created to verify if the
+// resource is already in the cache. If it is, it passes it to the callback.
+// Otherwise, it starts downloading the package. When the packaged resource has
+// been downloaded, its cache entry gets passed to the callback.
+class PackagedAppService final
+  : public nsIPackagedAppService
+{
+  NS_DECL_THREADSAFE_ISUPPORTS
+  NS_DECL_NSIPACKAGEDAPPSERVICE
+
+  PackagedAppService();
+
+private:
+  ~PackagedAppService();
+
+  // This method is called if an entry wasn't found in the cache.
+  // It checks to see if the package is currently being downloaded.
+  // If so, then it simply adds the callback to that PackageAppDownloader
+  // Else it begins downloading the new package and adds it to mDownloadingPackages
+  // - aURI is the packaged resource's URL
+  // - aCallback is the listener which gets called when the requested
+  //   resource is available.
+  // - aInfo is needed because cache entries are located in separate cache jars
+  //   If a resource isn't found in the package, aCallback->OnCacheEntryAvailable
+  //   will be called with a null entry and an error result as a status.
+  nsresult OpenNewPackageInternal(nsIURI *aURI,
+                                  nsICacheEntryOpenCallback *aCallback,
+                                  nsILoadContextInfo *aInfo);
+
+  // Called by PackageAppDownloader once the download has finished
+  // (or encountered an error) to remove the package from mDownloadingPackages
+  // Should be called on the main thread.
+  nsresult NotifyPackageDownloaded(nsCString aKey);
+
+  // This class is used to write data into the cache entry corresponding to the
+  // packaged resource being downloaded.
+  // The PackagedAppDownloader will hold a ref to a CacheEntryWriter that
+  // corresponds to the entry that is currently being downloaded.
+  class CacheEntryWriter final
+    : public nsIStreamListener
+  {
+  public:
+    NS_DECL_ISUPPORTS
+    NS_DECL_NSISTREAMLISTENER
+    NS_DECL_NSIREQUESTOBSERVER
+
+    // If successful, calling this static method will create a new
+    // CacheEntryWriter and will create the cache entry associated to the
+    // resource and open an output stream which we use for writing the resource's
+    // content into the cache entry.
+    static nsresult Create(nsIURI*, nsICacheStorage*, CacheEntryWriter**);
+
+    nsCOMPtr<nsICacheEntry> mEntry;
+  private:
+    CacheEntryWriter() { }
+    ~CacheEntryWriter() { }
+
+    // Static method used to write data into the cache entry
+    // Called from OnDataAvailable
+    static NS_METHOD ConsumeData(nsIInputStream *in, void *closure,
+                                 const char *fromRawSegment, uint32_t toOffset,
+                                 uint32_t count, uint32_t *writeCount);
+    // We write the data we read from the network into this stream which goes
+    // to the cache entry.
+    nsCOMPtr<nsIOutputStream> mOutputStream;
+  };
+
+  // This class is used to download a packaged app. It acts as a listener
+  // for the nsMultiMixedConv object that parses the package.
+  // There is an OnStartRequest, OnDataAvailable*, OnStopRequest sequence called
+  // for each resource
+  // The PackagedAppService holds a hash-table of the PackagedAppDownloaders
+  // that are in progress to coalesce same loads.
+  // Once the downloading is completed, it should call
+  // NotifyPackageDownloaded(packageURI), so the service releases the ref.
+  class PackagedAppDownloader final
+    : public nsIStreamListener
+  {
+  public:
+    NS_DECL_ISUPPORTS
+    NS_DECL_NSISTREAMLISTENER
+    NS_DECL_NSIREQUESTOBSERVER
+
+    // Initializes mCacheStorage and saves aKey as mPackageKey which is later
+    // used to remove this object from PackagedAppService::mDownloadingPackages
+    // - aKey is a string which uniquely identifies this package within the
+    //   packagedAppService
+    nsresult Init(nsILoadContextInfo* aInfo, const nsCString &aKey);
+    // Registers a callback which gets called when the given nsIURI is downloaded
+    // aURI is the full URI of a subresource, composed of packageURI + !// + subresourcePath
+    nsresult AddCallback(nsIURI *aURI, nsICacheEntryOpenCallback *aCallback);
+
+  private:
+    ~PackagedAppDownloader() { }
+
+    // Calls all the callbacks registered for the given URI.
+    // aURI is the full URI of a subresource, composed of packageURI + !// + subresourcePath
+    // It passes the cache entry and the result when calling OnCacheEntryAvailable
+    nsresult CallCallbacks(nsIURI *aURI, nsICacheEntry *aEntry, nsresult aResult);
+    // Clears all the callbacks for this package
+    // This would get called at the end of downloading the package and would
+    // cause us to call OnCacheEntryAvailable with a null entry. This would be
+    // equivalent to a 404 when loading from the net.
+    nsresult ClearCallbacks(nsresult aResult);
+    static PLDHashOperator ClearCallbacksEnumerator(const nsACString& key,
+      nsAutoPtr<nsCOMArray<nsICacheEntryOpenCallback>>& callbackArray,
+      void* arg);
+    // Returns a URI with the subresource's full URI
+    // The request must be QIable to nsIResponseHeadProvider since it looks
+    // at the Content-Location header to compute the full path.
+    static nsresult GetSubresourceURI(nsIRequest * aRequest, nsIURI **aResult);
+    // Used to write data into the cache entry of the resource currently being
+    // downloaded. It is kept alive until the downloader receives OnStopRequest
+    nsRefPtr<CacheEntryWriter> mWriter;
+    // Cached value of nsICacheStorage
+    nsCOMPtr<nsICacheStorage> mCacheStorage;
+    // A hastable containing all the consumers which requested a resource and need
+    // to be notified once it is inserted into the cache.
+    // The key is a subresource URI - http://example.com/package.pak!//res.html
+    // Should only be used on the main thread.
+    nsClassHashtable<nsCStringHashKey, nsCOMArray<nsICacheEntryOpenCallback>> mCallbacks;
+    // The key with which this package is inserted in
+    // PackagedAppService::mDownloadingPackages
+    nsCString mPackageKey;
+  };
+
+  // This class is used to check if a packaged resource has already been
+  // downloaded and saved into the cache.
+  // It calls aCallback->OnCacheEntryAvailable if the resource exists in the
+  // cache or PackagedAppService::OpenNewPackageInternal if it needs
+  // to be downloaded
+  class CacheEntryChecker final
+    : public nsICacheEntryOpenCallback
+  {
+  public:
+    NS_DECL_ISUPPORTS
+    NS_DECL_NSICACHEENTRYOPENCALLBACK
+
+    CacheEntryChecker(nsIURI *aURI, nsICacheEntryOpenCallback * aCallback,
+                      nsILoadContextInfo *aInfo)
+      : mURI(aURI)
+      , mCallback(aCallback)
+      , mLoadContextInfo(aInfo)
+    {
+    }
+  private:
+    ~CacheEntryChecker() { }
+
+    nsCOMPtr<nsIURI> mURI;
+    nsCOMPtr<nsICacheEntryOpenCallback> mCallback;
+    nsCOMPtr<nsILoadContextInfo> mLoadContextInfo;
+  };
+
+  // A hashtable of packages that are currently being downloaded.
+  // The key is a string formed by concatenating LoadContextInfo and package URI
+  // Should only be used on the main thread.
+  nsRefPtrHashtable<nsCStringHashKey, PackagedAppDownloader> mDownloadingPackages;
+};
+
+
+} // namespace net
+} // namespace mozilla
+
+#endif // mozilla_net_PackagedAppService_h
\ No newline at end of file
--- a/netwerk/protocol/http/moz.build
+++ b/netwerk/protocol/http/moz.build
@@ -29,16 +29,17 @@ EXPORTS += [
 ]
 
 EXPORTS.mozilla.net += [
     'HttpBaseChannel.h',
     'HttpChannelChild.h',
     'HttpChannelParent.h',
     'HttpInfo.h',
     'NullHttpChannel.h',
+    'PackagedAppService.h',
     'PHttpChannelParams.h',
     'PSpdyPush.h',
     'TimingStruct.h',
 ]
 
 # ASpdySession.cpp and nsHttpAuthCache cannot be built in unified mode because
 # they use plarena.h.
 SOURCES += [
@@ -73,16 +74,17 @@ UNIFIED_SOURCES += [
     'nsHttpHeaderArray.cpp',
     'nsHttpNTLMAuth.cpp',
     'nsHttpPipeline.cpp',
     'nsHttpRequestHead.cpp',
     'nsHttpResponseHead.cpp',
     'nsHttpTransaction.cpp',
     'NullHttpChannel.cpp',
     'NullHttpTransaction.cpp',
+    'PackagedAppService.cpp',
     'SpdyPush31.cpp',
     'SpdySession31.cpp',
     'SpdyStream31.cpp',
     'SpdyZlibReporter.cpp',
     'TunnelUtils.cpp',
 ]
 
 # These files cannot be built in unified mode because of OS X headers.
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit/test_packaged_app_service.js
@@ -0,0 +1,255 @@
+//
+// This file tests the packaged app service - nsIPackagedAppService
+// NOTE: The order in which tests are run is important
+//       If you need to add more tests, it's best to define them at the end
+//       of the file and to add them at the end of run_test
+//
+// ----------------------------------------------------------------------------
+//
+// test_bad_args
+//     - checks that calls to nsIPackagedAppService::requestURI do not accept a null argument
+// test_callback_gets_called
+//     - checks the regular use case -> requesting a resource should asynchronously return an entry
+// test_same_content
+//     - makes another request for the same file, and checks that the same content is returned
+// test_request_number
+//     - this test does not make a request, but checks that the package has only
+//       been requested once. The entry returned by the call to requestURI in
+//       test_same_content should be returned from the cache.
+//
+// test_package_does_not_exist
+//     - checks that requesting a file from a <package that does not exist>
+//       calls the listener with an error code
+// test_file_does_not_exist
+//     - checks that requesting a <subresource that doesn't exist> inside a
+//       package calls the listener with an error code
+//
+// test_bad_package
+//    - tests that a package with missing headers for some of the files
+//      will still return files that are correct
+// test_bad_package_404
+//    - tests that a request for a missing subresource doesn't hang if
+//      if the last file in the package is missing some headers
+
+Cu.import('resource://gre/modules/LoadContextInfo.jsm');
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// The number of times this package has been requested
+// This number might be reset by tests that use it
+var packagedAppRequestsMade = 0;
+// The default content handler. It just responds by sending the package data
+// with an application/package content type
+function packagedAppContentHandler(metadata, response)
+{
+  packagedAppRequestsMade++;
+  response.setHeader("Content-Type", 'application/package');
+  var body = testData.getData();
+  response.bodyOutputStream.write(body, body.length);
+}
+
+// The package content
+// getData formats it as described at http://www.w3.org/TR/web-packaging/#streamable-package-format
+var testData = {
+  content: [
+   { headers: ["Content-Location: /index.html", "Content-Type: text/html"], data: "<html>\r\n  <head>\r\n    <script src=\"/scripts/app.js\"></script>\r\n    ...\r\n  </head>\r\n  ...\r\n</html>\r\n", type: "text/html" },
+   { headers: ["Content-Location: /scripts/app.js", "Content-Type: text/javascript"], data: "module Math from '/scripts/helpers/math.js';\r\n...\r\n", type: "text/javascript" },
+   { headers: ["Content-Location: /scripts/helpers/math.js", "Content-Type: text/javascript"], data: "export function sum(nums) { ... }\r\n...\r\n", type: "text/javascript" }
+  ],
+  token : "gc0pJq0M:08jU534c0p",
+  getData: function() {
+    var str = "";
+    for (var i in this.content) {
+      str += "--" + this.token + "\r\n";
+      for (var j in this.content[i].headers) {
+        str += this.content[i].headers[j] + "\r\n";
+      }
+      str += "\r\n";
+      str += this.content[i].data + "\r\n";
+    }
+
+    str += "--" + this.token + "--";
+    return str;
+  }
+}
+
+XPCOMUtils.defineLazyGetter(this, "uri", function() {
+  return "http://localhost:" + httpserver.identity.primaryPort;
+});
+
+// The active http server initialized in run_test
+var httpserver = null;
+// The packaged app service initialized in run_test
+var paservice = null;
+// This variable is set before requestURI is called. The listener uses this variable
+// to check the correct resource path for the returned entry
+var packagePath = null;
+
+function run_test()
+{
+  // setup test
+  httpserver = new HttpServer();
+  httpserver.registerPathHandler("/package", packagedAppContentHandler);
+  httpserver.registerPathHandler("/304Package", packagedAppContentHandler);
+  httpserver.registerPathHandler("/badPackage", packagedAppBadContentHandler);
+  httpserver.start(-1);
+
+  paservice = Cc["@mozilla.org/network/packaged-app-service;1"]
+                     .getService(Ci.nsIPackagedAppService);
+  ok(!!paservice, "test service exists");
+
+  add_test(test_bad_args);
+
+  add_test(test_callback_gets_called);
+  add_test(test_same_content);
+  add_test(test_request_number);
+
+  add_test(test_package_does_not_exist);
+  add_test(test_file_does_not_exist);
+
+  add_test(test_bad_package);
+  add_test(test_bad_package_404);
+
+  // run tests
+  run_next_test();
+}
+
+// This checks the proper metadata is on the entry
+var metadataListener = {
+  onMetaDataElement: function(key, value) {
+    if (key == 'response-head')
+      equal(value, "HTTP/1.1 200 \r\nContent-Location: /index.html\r\nContent-Type: text/html\r\n");
+    else if (key == 'request-method')
+      equal(value, "GET");
+    else
+      ok(false, "unexpected metadata key")
+  }
+}
+
+// A listener we use to check the proper cache entry is returned by the service
+// NOTE: this listener only checks the content of index.html
+//       Don't use it when requesting other packaged resources! :)
+var cacheListener = {
+  onCacheEntryCheck: function() { return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; },
+  onCacheEntryAvailable: function (entry, isnew, appcache, status) {
+    ok(!!entry, "Needs to have an entry");
+    equal(status, Cr.NS_OK, "status is NS_OK");
+    equal(entry.key, uri + packagePath + "!//index.html", "Check entry has correct name");
+    entry.visitMetaData(metadataListener);
+    var inputStream = entry.openInputStream(0);
+    pumpReadStream(inputStream, function(read) {
+        inputStream.close();
+        equal(read,"<html>\r\n  <head>\r\n    <script src=\"/scripts/app.js\"></script>\r\n    ...\r\n  </head>\r\n  ...\r\n</html>\r\n"); // not using do_check_eq since logger will fail for the 1/4MB string
+    });
+    run_next_test();
+  }
+};
+
+// ----------------------------------------------------------------------------
+
+// These calls should fail, since one of the arguments is invalid or null
+function test_bad_args() {
+  Assert.throws(() => { paservice.requestURI(createURI("http://test.com"), LoadContextInfo.default, cacheListener); }, "url's with no !// aren't allowed");
+  Assert.throws(() => { paservice.requestURI(createURI("http://test.com/package!//test"), LoadContextInfo.default, null); }, "should have a callback");
+  Assert.throws(() => { paservice.requestURI(null, LoadContextInfo.default, cacheListener); }, "should have a URI");
+  Assert.throws(() => { paservice.requestURI(createURI("http://test.com/package!//test"), null, cacheListener); }, "should have a LoadContextInfo");
+  run_next_test();
+}
+
+// ----------------------------------------------------------------------------
+
+// This tests that the callback gets called, and the cacheListener gets the proper content.
+function test_callback_gets_called() {
+  packagePath = "/package";
+  paservice.requestURI(createURI(uri + packagePath + "!//index.html"), LoadContextInfo.default, cacheListener);
+}
+
+// Tests that requesting the same resource returns the same content
+function test_same_content() {
+  packagePath = "/package";
+  paservice.requestURI(createURI(uri + packagePath + "!//index.html"), LoadContextInfo.default, cacheListener);
+}
+
+// Check the package has only been requested once.
+function test_request_number() {
+  equal(packagedAppRequestsMade, 1, "only one request should be made. Second should be loaded from cache");
+  run_next_test();
+}
+
+// ----------------------------------------------------------------------------
+
+// This listener checks that the requested resources are not returned
+// either because the package does not exist, or because the requested resource
+// is not contained in the package.
+var listener404 = {
+  onCacheEntryCheck: function() { return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; },
+  onCacheEntryAvailable: function (entry, isnew, appcache, status) {
+    // XXX: it returns NS_ERROR_FAILURE for a missing package
+    // and NS_ERROR_FILE_NOT_FOUND for a missing file from the package.
+    // Maybe make them both return NS_ERROR_FILE_NOT_FOUND?
+    notEqual(status, Cr.NS_OK, "NOT FOUND");
+    ok(!entry, "There should be no entry");
+    run_next_test();
+  }
+};
+
+// Tests that an error is returned for a non existing package
+function test_package_does_not_exist() {
+  packagePath = "/package_non_existent";
+  paservice.requestURI(createURI(uri + packagePath + "!//index.html"), LoadContextInfo.default, listener404);
+}
+
+// Tests that an error is returned for a non existing resource in a package
+function test_file_does_not_exist() {
+  packagePath = "/package"; // This package exists
+  paservice.requestURI(createURI(uri + packagePath + "!//file_non_existent.html"), LoadContextInfo.default, listener404);
+}
+
+// ----------------------------------------------------------------------------
+
+// Broken package. The first and last resources do not contain a "Content-Location" header
+// and should be ignored.
+var badTestData = {
+  content: [
+   { headers: ["Content-Type: text/javascript"], data: "module Math from '/scripts/helpers/math.js';\r\n...\r\n", type: "text/javascript" },
+   { headers: ["Content-Location: /index.html", "Content-Type: text/html"], data: "<html>\r\n  <head>\r\n    <script src=\"/scripts/app.js\"></script>\r\n    ...\r\n  </head>\r\n  ...\r\n</html>\r\n", type: "text/html" },
+   { headers: ["Content-Type: text/javascript"], data: "export function sum(nums) { ... }\r\n...\r\n", type: "text/javascript" }
+  ],
+  token : "gc0pJq0M:08jU534c0p",
+  getData: function() {
+    var str = "";
+    for (var i in this.content) {
+      str += "--" + this.token + "\r\n";
+      for (var j in this.content[i].headers) {
+        str += this.content[i].headers[j] + "\r\n";
+      }
+      str += "\r\n";
+      str += this.content[i].data + "\r\n";
+    }
+
+    str += "--" + this.token + "--";
+    return str;
+  }
+}
+
+// Returns the content of the package with "Content-Location" headers missing for the first and last resource
+function packagedAppBadContentHandler(metadata, response)
+{
+  response.setHeader("Content-Type", 'application/package');
+  var body = badTestData.getData();
+  response.bodyOutputStream.write(body, body.length);
+}
+
+// Checks that the resource with the proper headers inside the bad package is still returned
+function test_bad_package() {
+  packagePath = "/badPackage";
+  paservice.requestURI(createURI(uri + packagePath + "!//index.html"), LoadContextInfo.default, cacheListener);
+}
+
+// Checks that the request for a non-existent resource doesn't hang for a bad package
+function test_bad_package_404() {
+  packagePath = "/badPackage";
+  paservice.requestURI(createURI(uri + packagePath + "!//file_non_existent.html"), LoadContextInfo.default, listener404);
+}
+
+// ----------------------------------------------------------------------------
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -311,8 +311,9 @@ skip-if = os != "win"
 [test_reply_without_content_type.js]
 [test_websocket_offline.js]
 [test_tls_server.js]
 # The local cert service used by this test is not currently shipped on Android
 skip-if = os == "android"
 [test_1073747.js]
 [test_multipart_streamconv_application_package.js]
 [test_safeoutputstream_append.js]
+[test_packaged_app_service.js]