Downloading items from direct download links with browser.downloads.download()?

Hello all.
I recently switched from Chrome to Firefox and I moved my extension along with it, it’s basically a file renamer for downloading items.

Issue arises when I pass items that uses a direct download link through the downloads api.

    <html>
    <a id="exampleItem" href="/file/url/that/click/to/download">
    </html>
    ------In the background page-------
   browser.downloads.download({
       url: "/file/url/that/click/to/download",
        name: fileName
    });

I tried different sources, websites, and a chromium-based browser, and I have concluded the API just won’t accept the direct download link.

Any ideas how do I approach this or if I did something wrong?

Is there some error in the console?
Also, can you paste here a bit more code here? And to activate code formatting, place ``` on the line before and after the code.

Also check the docs just in case:

1 Like

@ursa_batter, can you share a minimal reproduction of the issue? I just put together a demo to make sure that downloading a well known URL works as expected (it does). To use this, these files to a local directory, load them as a temporary extension in Firefox, then in the Extensions menu click the “Direct download” entry.

manifest.json

{
  "name": "Direct download",
  "version": "1.0",
  "manifest_version": 3,
  "background": {
    "scripts": ["background.js"],
    "service_worker": "background.js"
  },
  "permissions": [
    "downloads"
  ],
  "action": {}
}

background.js

// Minimal polyfill to make this work in Chrome and Firefox
globalThis.browser ??= chrome;

browser.action.onClicked.addListener(() => {
  browser.downloads.download({
    url: "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png",
    filename: "google-logo.png",
  });
});

Sorry, I have edited now

But that was effectively the only code that matters, in the backgrounds page.

/* Logic pass from website to background.js */
browser.downloads.download({
      url: "https://drive.google.com/uc?export=download&id=1uH3s6XAtzjoMdgqNGRM4bpErmi6073yx",
      filename: "google-logo.png",
      saveAs: givenStorageObj.individualSave
    });

My replies to you keeps getting temporary hidden, so I tried a few replies, all the hidden ones are variations based on this:

Here is a webp image of a windows XP screenshot I found online, uploaded on google drive, and edited to become a direct download link:

https://drive.google.com/uc?export=download&id=1uH3s6XAtzjoMdgqNGRM4bpErmi6073yx

So the only thing your code, effectively the same as mine will have to change:

globalThis.browser ??= chrome;

browser.action.onClicked.addListener(() => {
  browser.downloads.download({
    url: "https://drive.google.com/uc?export=download&id=1uH3s6XAtzjoMdgqNGRM4bpErmi6073yx",
    filename: "google-logo.png",
  });
});

Testing the same link and filename, the filenames are changed correctly when downloading, the passed link is hardcoded, but fails on delivery.

Here is a webp image of a windows XP screenshot I found online, uploaded on google drive, and edited to become a direct download link:

https://drive.google.com/uc?export=download&id=1uH3s6XAtzjoMdgqNGRM4bpErmi6073yx

So the only thing your code, effectively the same as mine will have to change:

globalThis.browser ??= chrome;

browser.action.onClicked.addListener(() => {
  browser.downloads.download({
    url: "https://drive.google.com/uc?export=download&id=1uH3s6XAtzjoMdgqNGRM4bpErmi6073yx",
    filename: "google-logo.png",
  });
});

Testing the same link and filename, the filenames are changed correctly when downloading, the passed link is hardcoded, but fails on delivery.

Screenshot of the download

Huh, the Discourse server really didn’t like whatever you were doing in those posts. Very weird.

Anyway, I tried your modified version of the downloads.download() call and observed that the call failed. I opened the Browser Console (Firefox Menu > More Tools > Browser Console), triggered the download again, and received the following error:

Error: Got a request https://drive.google.com/uc?export=download&id=1uH3s6XAtzjoMdgqNGRM4bpErmi6073yx without a browsingContextID set

Okay, that’s progress. Based on the stack trace we know that something is going wrong with the network request. The error references a context ID, which makes me think there’s an issue with the environment being used to execute the request but it’s not clear what. Let’s take a look at the documentation for the downloads.download() method to see if there are any clues.

There are a few properties on the options object that could be related to the request context, but for whatever reason the first one that stood out to me was allowHttpErrors. Let’s set that to true and see what we get.

Now a file is downloaded, but the image won’t open. If we open the downloaded file in a text editor, we’ll see something like this:

<html lang="en" dir=ltr><meta charset=utf-8><meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width"><title>Error 403 (Forbidden)!!1</title><style nonce="REDACTED">...</style><main id="af-error-container" role="main"><a href=//www.google.com><span id=logo aria-label=Google role=img></span></a><p><b>403.</b> <ins>That’s an error.</ins><p>We're sorry, but you do not have access to this page. <ins>That’s all we know.</ins></main>

The body text clearly states that the server responded with a 403 error, indicating that the requester didn’t have permissions to perform the request. If we perform the same request on the command line using curl, we’ll see a very similar result, but this time it’s a 400:

% curl -v https://drive.google.com/uc?export=download&id=1uH3s6XAtzjoMdgqNGRM4bpErmi6073yx
[1] 83847
% * Host drive.google.com:443 was resolved.
* IPv6: (none)
* IPv4: 142.250.189.238
*   Trying 142.250.189.238:443...
* Connected to drive.google.com (142.250.189.238) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=*.google.com
*  start date: Sep 24 02:46:05 2024 GMT
*  expire date: Dec 17 02:46:04 2024 GMT
*  subjectAltName: host "drive.google.com" matched cert's "*.google.com"
*  issuer: C=US; O=Google Trust Services; CN=WR2
*  SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://drive.google.com/uc?export=download
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: drive.google.com]
* [HTTP/2] [1] [:path: /uc?export=download]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET /uc?export=download HTTP/2
> Host: drive.google.com
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/2 400 
< content-type: text/html; charset=utf-8
< cache-control: no-cache, no-store, max-age=0, must-revalidate
< pragma: no-cache
< expires: Mon, 01 Jan 1990 00:00:00 GMT
< date: Tue, 15 Oct 2024 16:54:52 GMT
< strict-transport-security: max-age=31536000
< accept-ch: Sec-CH-UA-Arch, Sec-CH-UA-Bitness, Sec-CH-UA-Full-Version, Sec-CH-UA-Full-Version-List, Sec-CH-UA-Model, Sec-CH-UA-WoW64, Sec-CH-UA-Form-Factors, Sec-CH-UA-Platform, Sec-CH-UA-Platform-Version
< content-security-policy: script-src 'nonce-JlWDKTg8WHRQqPQAKeoPdg' 'unsafe-inline';object-src 'none';base-uri 'self';report-uri /_/DriveUntrustedContentHttp/cspreport;worker-src 'self'
< content-security-policy: require-trusted-types-for 'script';report-uri /_/DriveUntrustedContentHttp/cspreport
< permissions-policy: ch-ua-arch=*, ch-ua-bitness=*, ch-ua-full-version=*, ch-ua-full-version-list=*, ch-ua-model=*, ch-ua-wow64=*, ch-ua-form-factors=*, ch-ua-platform=*, ch-ua-platform-version=*
< cross-origin-opener-policy: same-origin
< server: ESF
< x-xss-protection: 0
< x-content-type-options: nosniff
< alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
< accept-ranges: none
< vary: Accept-Encoding
< 
* Connection #0 to host drive.google.com left intact
<html lang="en" dir=ltr><meta charset=utf-8><meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width"><title>Error 400 (Bad Request)!!1</title><style nonce="REDACTED">...</style><main id="af-error-container" role="main"><a href=//www.google.com><span id=logo aria-label=Google role=img></span></a><p><b>400.</b> <ins>That’s an error.</ins><p>The server cannot process the request because it is malformed. It should not be retried. <ins>That’s all we know.</ins></main>
[1]  + done       curl -v https://drive.google.com/uc?export=download

Notably, though, the request completes as expected if you paste the URL in the address bar. You can also see that if you open devtools before navigating to the URL pasted in the URL bar, you can see that the drive servers respond to the initial request with a 303 “See other” redirect.

Together, these responses make me suspect that there’s an issue with how the download() method constructs the request. There may be a good reason for that, but someone will need to dig deeper in order to determine that. I’ll open a bug report.

Bug filed at https://bugzilla.mozilla.org/show_bug.cgi?id=1924835

Wow, that was expert and very through.

I guess it probably is difference in how chrome.downloads is constructed compared to browser.downloads.

Thank you so much.