Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ The URL format looks like `https://github.com/<FORK>/bazel/releases/download/<VE

You can also override the URL by setting the environment variable `$BAZELISK_BASE_URL`. Bazelisk will then append `/<VERSION>/<FILENAME>` to the base URL instead of using the official release server. Bazelisk will read file [`~/.netrc`](https://everything.curl.dev/usingcurl/netrc) for credentials for Basic authentication.

If you want to use the releases stored on the local disk, set the URL as `file://` followed by the local disk path. On Windows, escape `\` in the path by `%5C`.
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README documentation suggests using %5C to escape backslashes in Windows paths, but this is misleading. The proper way to specify Windows paths in file URLs is to use forward slashes, like file:///C:/path/to/folder. URL-encoding backslashes as %5C would result in the literal string containing backslashes after URL decoding, which may not work correctly. Consider revising the documentation to recommend using forward slashes in file URLs for all platforms, including Windows, as this is the standard approach for file URLs.

Copilot uses AI. Check for mistakes.

If for any reason none of this works, you can also override the URL format altogether by setting the environment variable `$BAZELISK_FORMAT_URL`. This variable takes a format-like string with placeholders and performs the following replacements to compute the download URL:

- `%e`: Extension suffix, such as the empty string or `.exe`.
Expand Down
39 changes: 39 additions & 0 deletions httputil/httputil.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"path/filepath"
"regexp"
"strconv"
"strings"
"time"

netrc "github.com/bgentry/go-netrc/netrc"
Expand Down Expand Up @@ -78,7 +79,41 @@ func ReadRemoteFile(url string, auth string) ([]byte, http.Header, error) {
return body, res.Header, nil
}

Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LocalFileError type lacks a documentation comment. According to Go conventions, exported types should have documentation comments. Consider adding a comment like: LocalFileError wraps errors that occur when reading local files via file:// URLs, allowing them to be distinguished from network errors for retry logic.

Suggested change
// LocalFileError wraps errors that occur when reading local files via file:// URLs,
// allowing them to be distinguished from network errors for retry logic.

Copilot uses AI. Check for mistakes.
type LocalFileError struct{ err error }

func (e *LocalFileError) Error() string { return e.err.Error() }
func (e *LocalFileError) Unwrap() error { return e.err }

// Handles file:// URLs by reading files from disk.
func readLocalFile(urlStr string) (*http.Response, error) {
urlStr = strings.TrimPrefix(urlStr, "file://")
path, err := url.PathUnescape(urlStr)
if err != nil {
return nil, &LocalFileError{err: fmt.Errorf("invalid file url %q: %w", urlStr, err)}
}
Comment on lines +88 to +93
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file URL parsing implementation has cross-platform issues. On Windows, file URLs use the format file:///C:/path/to/file (note three slashes). After stripping file://, this leaves /C:/path/to/file, which is not a valid Windows path. The code should use url.Parse to properly extract the path component, which automatically handles platform-specific conversions. For example, url.Parse("file:///C:/path/file").Path would return /C:/path/file on Unix but C:/path/file on Windows when using proper URL parsing libraries. Consider using url.Parse(urlStr) followed by accessing parsed.Path to get the correct file path.

Copilot uses AI. Check for mistakes.
f, err := os.Open(path)
if err != nil {
return nil, &LocalFileError{err: fmt.Errorf("could not open %q: %w", path, err)}
}
Comment on lines +88 to +97
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function doesn't handle edge cases for malformed file URLs. For example, if the URL is just file:// (with no path), the code will attempt to open an empty path which could lead to confusing error messages. Consider adding validation to check for empty or invalid paths after URL parsing.

Copilot uses AI. Check for mistakes.
var size int64 = -1
if fi, statErr := f.Stat(); statErr == nil {
size = fi.Size()
}
return &http.Response{
StatusCode: 200,
Status: "200 OK",
Header: make(http.Header),
Body: f,
ContentLength: size,
Request: &http.Request{Method: "GET", URL: &url.URL{Scheme: "file", Path: path}},
}, nil
}

func get(url, auth string) (*http.Response, error) {
if strings.HasPrefix(url, "file://") {
return readLocalFile(url)
}

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("could not create request: %v", err)
Expand Down Expand Up @@ -127,6 +162,10 @@ func get(url, auth string) (*http.Response, error) {
func shouldRetry(res *http.Response, err error) bool {
// Retry if the client failed to speak HTTP.
if err != nil {
var nre *LocalFileError
if errors.As(err, &nre) {
Comment on lines +165 to +166
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable name nre is unclear and doesn't clearly indicate it represents a LocalFileError. Consider using a more descriptive name like localFileErr or fileErr to improve code readability.

Suggested change
var nre *LocalFileError
if errors.As(err, &nre) {
var localFileErr *LocalFileError
if errors.As(err, &localFileErr) {

Copilot uses AI. Check for mistakes.
return false
}
return true
}
// For HTTP: only retry on non-permanent/fatal errors.
Expand Down
42 changes: 42 additions & 0 deletions httputil/httputil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package httputil
import (
"errors"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
Expand Down Expand Up @@ -251,3 +254,42 @@ func TestNoRetryOnPermanentError(t *testing.T) {
t.Fatalf("Expected no retries for permanent error, but got %d", clock.TimesSlept())
}
}

func TestReadLocalFile(t *testing.T) {
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "payload.txt")
want := "hello from disk"
if err := os.WriteFile(path, []byte(want), 0644); err != nil {
t.Fatalf("failed to write temp file: %v", err)
}
fileURL := (&url.URL{Scheme: "file", Path: path}).String()
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses url.URL construction to create file URLs, which handles platform-specific differences correctly. However, the actual implementation in readLocalFile doesn't parse URLs the same way. This could cause the test to pass on Unix-like systems but fail on Windows. Consider adding a Windows-specific test case or ensuring the implementation matches the URL construction approach used in tests.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback


body, _, err := ReadRemoteFile(fileURL, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := string(body); got != want {
t.Fatalf("expected body %q, but got %q", want, got)
}
}

func TestReadLocalFileNotFound(t *testing.T) {
clock := newFakeClock()
RetryClock = clock
MaxRetries = 10
MaxRequestDuration = time.Hour

missingPath := filepath.Join(t.TempDir(), "does-not-exist.txt")
fileURL := (&url.URL{Scheme: "file", Path: missingPath}).String()

_, _, err := ReadRemoteFile(fileURL, "")
if err == nil {
t.Fatal("expected error for missing file")
}
if !errors.Is(err, os.ErrNotExist) {
t.Fatalf("expected os.ErrNotExist, got %v", err)
}
if clock.TimesSlept() != 0 {
t.Fatalf("expected no retries for file:// error, but slept %d times", clock.TimesSlept())
}
}
Loading