Skip to content

[BUG]: A download that answers 403, 429 or 500 is saved as the archive, and the install ends in "Checksum error" #746

Open

Description

@linghengqian

Update (2026-11-05)

The statuses are fixed on main by #747 (4ddd721). It is not in a release yet, the latest is 1.0.13. Every download path now fails on a status other than 200 before it opens the file, and the error names the status and the URL. With the files below, the 404 is unchanged and vfox install repro@403 ends like this:

Preinstalling repro@403...
failed to download repro@403 file, err:unexpected HTTP status 403 Forbidden for http://127.0.0.1:18080/403/repro.zip
failed to install repro

The 429 and the 500 print the same lines with their own status, and so does REPRO_NO_SUM=1 vfox install repro@403. The checksum and the unpacking no longer see the page of HTML.

What is not fixed is the User-Agent section at the end. This issue stays open for it, and the comment below lists where it applies.


Version
vfox 1.0.13 (release binary) and main at cdf0747 (2026-11-02, RuntimeVersion is still 1.0.13). The two builds print the same lines in everything below.

OS
Linux amd64, in a golang:1.27.1 container. The code involved depends on neither the OS nor the shell.

Describe the bug
Download in internal/sdk/sdk.go fails on one status only, 404 (source file not found). For any other status, 403, 429, 500 and so on, it saves the body as the archive and goes on, so the install fails one step later on a page of HTML, and nothing in the output says the server refused the download or with which status:

  • With a checksum from PreInstall: Checksum error, file: …/repro.zip, then checksum error.
  • Without one: WARNING: Checksum is not provided, skip verify..., then unpack failed, err:zip: not a valid zip file.

This is not hypothetical. Huawei Cloud's mirror of Gradle answers a download from Go-http-client/1.1 with a 403 and a page of HTML, and serves the same file to other agents. With the vfox-gradle plugin and VFOX_GRADLE_MIRRORS=https://repo.huaweicloud.com/gradle, vfox install gradle@8.7 ends in Checksum error, and a user cannot tell that it was an HTTP 403 from the mirror.

Steps to reproduce
No plugin from the registry is needed. A plugin of four small files, whose PreInstall gets the digest with http.get as most plugins do, and a server that answers the status that is the first part of the path.

metadata.lua:

PLUGIN = {}
PLUGIN.name = "repro"
PLUGIN.version = "0.0.1"
PLUGIN.homepage = "https://example.invalid"
PLUGIN.license = "Apache 2.0"
PLUGIN.description = "A download that answers something else than 200 or 404"

hooks/available.lua:

function PLUGIN:Available(ctx)
    return { { version = "403" } }
end

hooks/env_keys.lua:

function PLUGIN:EnvKeys(ctx)
    return { { key = "PATH", value = ctx.path } }
end

hooks/pre_install.lua:

local http = require("http")

-- The version is the status the server answers the download with: 403, 500, ...
function PLUGIN:PreInstall(ctx)
    local base = "http://127.0.0.1:18080/" .. ctx.version
    local result = { version = ctx.version, url = base .. "/repro.zip" }
    if os.getenv("REPRO_NO_SUM") == nil then
        -- A request the plugin makes itself, as most plugins do for a checksum.
        local resp = http.get({ url = base .. "/repro.zip.sha256" })
        result.sha256 = resp.body
    end
    return result
end

server.go:

package main

import (
	"fmt"
	"log"
	"net/http"
	"strconv"
	"strings"
)

// /<status>/repro.zip answers <status> with a page of HTML, and
// /<status>/repro.zip.sha256 answers 200 with a digest of 64 zeros.
func main() {
	log.SetFlags(0)
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		log.Printf("%s %s  User-Agent: %s", r.Method, r.URL.Path, r.UserAgent())
		parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
		status, err := strconv.Atoi(parts[0])
		if err != nil || len(parts) != 2 {
			http.NotFound(w, r)
			return
		}
		if strings.HasSuffix(parts[1], ".sha256") {
			fmt.Fprint(w, strings.Repeat("0", 64))
			return
		}
		w.Header().Set("Content-Type", "text/html")
		w.WriteHeader(status)
		fmt.Fprintf(w, "<html><body>HTTP %d</body></html>", status)
	})
	log.Fatal(http.ListenAndServe("127.0.0.1:18080", nil))
}
$ go run server.go &
$ zip -r repro.zip metadata.lua hooks
$ vfox add repro --source ./repro.zip
$ vfox install repro@404
$ vfox install repro@403
$ vfox install repro@500
$ REPRO_NO_SUM=1 vfox install repro@403

Actual behavior
The 404 is reported as what it is:

$ vfox install repro@404
Preinstalling repro@404...
failed to download repro@404 file, err:source file not found
failed to install repro

The 403 is not, and the 500 and the 429 print the same lines as the 403:

$ vfox install repro@403
Preinstalling repro@403...
Verifying checksum 0000000000000000000000000000000000000000000000000000000000000000...
Checksum error, file: …/.vfox/cache/repro/repro.zip
checksum error
failed to install repro

Without a checksum:

$ REPRO_NO_SUM=1 vfox install repro@403
Preinstalling repro@403...
Verifying checksum ...
WARNING: Checksum is not provided, skip verify...
Unpacking …/.vfox/cache/repro/repro.zip...
unpack failed, err:zip: not a valid zip file
failed to install repro

Expected behavior
Like the 404, a status outside 2xx fails the download itself, naming the status and the URL, for example failed to download repro@403 file, err:unexpected HTTP status 403 Forbidden. Nothing is saved, so the checksum and the unpacking never see a page of HTML.

Where
Download is called by moveRemoteFile, which wraps its error as failed to download <label> file, err:…. The 404 is the only status it looks at, and the file is opened and written right after:

vfox/internal/sdk/sdk.go

Lines 1060 to 1084 in cdf0747

func (b *impl) Download(u *url.URL, headers map[string]string) (string, error) {
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return "", err
}
for key, value := range headers {
req.Header.Add(key, value)
}
resp, err := b.envContext.HttpClient().Do(req)
if err != nil {
var urlErr *url.Error
if errors.As(err, &urlErr) {
var netErr net.Error
if errors.As(urlErr.Err, &netErr) && netErr.Timeout() {
return "", errors.New("request timeout")
}
}
return "", err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return "", errors.New("source file not found")
}

	if resp.StatusCode == http.StatusNotFound {
		return "", errors.New("source file not found")
	}

A check of resp.StatusCode for 2xx after it, before the file is opened, would do. (Downloader.Download in internal/shared/util/downloader.go has the same check, but nothing calls NewDownloader.)

Related: the User-Agent of a download
The requests a plugin makes with http.get carry vfox/1.0.13 vfox-repro/0.0.1 (ensureUserAgent and ComputeUserAgent), but the download goes out as Go-http-client/1.1, unless PreInstall returns headers. The log of the server above shows both:

GET /403/repro.zip.sha256  User-Agent: vfox/1.0.13 vfox-repro/0.0.1
GET /403/repro.zip  User-Agent: Go-http-client/1.1

Mirrors and CDNs that refuse Go's default agent, like the one above, are what turns this into the bug. A plugin can work around it by returning headers = { ["User-Agent"] = … } from PreInstall, which vfox applies to the download: with it, the install through Huawei Cloud's mirror above succeeds and passes the checksum. If the download used the agent that the plugin's own requests use, no plugin would have to. I can split this into an issue of its own if you prefer.

Activity

  1. linghengqian commented on Nov 3, 2026

    @linghengqian
    MemberAuthor

    See #747 .

  2. added this to the 1.0.14 milestone on Nov 4, 2026
  3. linghengqian commented on Nov 5, 2026

    @linghengqian
    MemberAuthor

    #747 is merged (4ddd721) and fixes the first part of this issue, the statuses. The User-Agent section is not touched, and it applies to more than the download of an SDK.

    On main at 4ddd721, with a local server that logs the agent of each request, the requests that a plugin makes with http.get carry the vfox agent. The requests that vfox makes itself go out as Go's default:

    vfox available (registry index)             Go-http-client/1.1
    vfox add <name> (registry manifest)         Go-http-client/1.1
    vfox add <name> (plugin download)           Go-http-client/1.1
    vfox add --source <url> (plugin download)   Go-http-client/1.1
    vfox update (registry manifest)             Go-http-client/1.1
    vfox update (manifestUrl manifest)          Go-http-client/1.1
    vfox update (updateUrl or plugin download)  Go-http-client/1.1
    vfox install (SDK archive)                  Go-http-client/1.1
    http.get of a plugin during vfox install    vfox/1.0.13 vfox-demo/0.0.1
    

    Only ensureUserAgent sets a User-Agent (internal/plugin/luai/module/http/http.go:110, :163, :223, for http.get, http.head and http.download_file). These requests are built and sent without one:

    • Sdk.Download, internal/sdk/sdk.go:1070-1077: the SDK archive, the case of this issue.
    • fetchPluginManifest, internal/manager.go:389: registry and manifestUrl manifests.
    • downloadPlugin, internal/manager.go:445-451: plugins added or updated from a URL or the registry.
    • Available, internal/manager.go:671: the registry index.
    • fetchLatestVersion and downloadFile, cmd/commands/upgrade.go:45 and :104: vfox upgrade. Not run, since it talks to github.com; the code sets no header there either.
    • Downloader.Download, internal/shared/util/downloader.go:36-40, which nothing calls.

    A mirror that refuses Go's default agent, like the one above, gets that agent from all of these. vfox-gradle returns headers from PreInstall to send its own (lib/util.lua), and a plugin cannot do that for the registry or for vfox upgrade.

    Still open:

    • The download of an SDK sends the agent of the plugin's own requests (ComputeUserAgent in internal/plugin/luai/context.go), and a User-Agent returned in the headers of PreInstall still wins.
    • The requests that vfox makes for itself send vfox/<version>, which ComputeUserAgent returns when it gets no plugin name.
  4. ManasBhole commented on Nov 5, 2026

    @ManasBhole

    Hi! Is the remaining User-Agent part of this issue still open to be taken? I'd like to work on it.

  5. linghengqian commented on Nov 5, 2026

    @linghengqian
    MemberAuthor

    Hi! Is the remaining User-Agent part of this issue still open to be taken? I'd like to work on it.

    The User-Agent part is still unresolved;
    Feel free to open a PR

  6. removed this from the 1.0.14 milestone on Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions