Repository navigation
Description
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, thenchecksum error. - Without one:
WARNING: Checksum is not provided, skip verify..., thenunpack 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" } }
endhooks/env_keys.lua:
function PLUGIN:EnvKeys(ctx)
return { { key = "PATH", value = ctx.path } }
endhooks/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
endserver.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@403Actual 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:
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
linghengqian commentedon Nov 3, 2026
See #747 .
linghengqian commentedon Nov 5, 2026
#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
mainat 4ddd721, with a local server that logs the agent of each request, the requests that a plugin makes withhttp.getcarry the vfox agent. The requests that vfox makes itself go out as Go's default:Only
ensureUserAgentsets a User-Agent (internal/plugin/luai/module/http/http.go:110,:163,:223, forhttp.get,http.headandhttp.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 andmanifestUrlmanifests.downloadPlugin,internal/manager.go:445-451: plugins added or updated from a URL or the registry.Available,internal/manager.go:671: the registry index.fetchLatestVersionanddownloadFile,cmd/commands/upgrade.go:45and: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-gradlereturnsheadersfromPreInstallto send its own (lib/util.lua), and a plugin cannot do that for the registry or forvfox upgrade.Still open:
ComputeUserAgentininternal/plugin/luai/context.go), and aUser-Agentreturned in theheadersofPreInstallstill wins.vfox/<version>, whichComputeUserAgentreturns when it gets no plugin name.ManasBhole commentedon Nov 5, 2026
Hi! Is the remaining User-Agent part of this issue still open to be taken? I'd like to work on it.
linghengqian commentedon Nov 5, 2026
The User-Agent part is still unresolved;
Feel free to open a PR