[GH-ISSUE #2130] Regression: UploadObject Ignores -external-url and downgrades to HTTP in v1.53.0 #254

Closed
opened 2026-03-03 12:09:27 +03:00 by kerem · 1 comment
Owner

Originally created by @tobiasf-ps on GitHub (Feb 2, 2026).
Original GitHub issue: https://github.com/fsouza/fake-gcs-server/issues/2130

Issue

I've identified a regression in v1.53.0 that breaks HTTPS resumable uploads. When the server is started with a https -external-url, the initial POST to initiate a resumable upload correctly returns a 200 OK, but the Location header in that response incorrectly specifies a http:// URL, ignoring the -external-url setting.

When the client follows this incorrect http:// URL for the subsequent PUT request, the server (correctly) rejects it with a 400 Bad Request and the error Client sent an HTTP request to an HTTPS server.

This behavior did not exist in v1.52.3, where the Location header correctly respected the https:// scheme from the -external-url flag.

Below is a sample (redacted) request/response I extracted.

Request (note: https)

URI: POST **https**://localhost:4443/upload/storage/v1/b/xxx/o?uploadType=resumable
Headers: X-Upload-Content-Type: application/json
X-Upload-Content-Length: 4382
User-Agent: google-api-dotnet-client/1.72.0.0 (gzip)
Body: xxxx

Response (note: http)

Status Code: 200 OK
Headers: Location: **http**://localhost:4443/upload/storage/v1/b/xxx/o?uploadType=resumable&name=xxx&upload_id=xxx
Date: Mon, 02 Feb 2026 17:52:28 GMT
Body: {"kind":"storage#object","name":"xxx","id":"xxx","bucket":"xxx","size":"0","contentType":"application/json","acl":[{"bucket":"xxx","entity":"projectOwner-xxx","etag":"RVRhZw==","kind":"storage#objectAccessControl","object":"xxx","projectTeam":{},"role":"OWNER"}],"storageClass":"STANDARD","generation":"0","selfLink":"**http**://localhost:4443/storage/v1/b/xxx/o/xxx","mediaLink":"**http**://localhost:4443/download/storage/v1/b/xxx/o/markets.json?alt=media","metageneration":"1"}

Root cause

I think this is due to the new urlhelper.GetBaseURL function attempting to determine the scheme from the request. However, for a direct HTTPS connection (without a reverse proxy), the X-Forwarded-Proto header is not present, and the r.URL.Scheme field on the incoming http.Request is empty. This causes the helper to fall back to a hardcoded "http" scheme, which is incorrect.

I'm not a Go developer so I'm not able to suggest a fix myself... Gemini has helped me with this though - do with it what you will :P

func getScheme(r *http.Request) string {
	if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
		return proto
	}
    // Add this check
	if r.TLS != nil {
		return "https"
	}
	if r.URL.Scheme != "" {
		return r.URL.Scheme
	}
	return "http"
}

Thank you for your work on this project!

Originally created by @tobiasf-ps on GitHub (Feb 2, 2026). Original GitHub issue: https://github.com/fsouza/fake-gcs-server/issues/2130 ## Issue I've identified a regression in v1.53.0 that breaks HTTPS resumable uploads. When the server is started with a https `-external-url`, the initial POST to initiate a resumable upload correctly returns a 200 OK, but the Location header in that response incorrectly specifies a http:// URL, ignoring the -external-url setting. When the client follows this incorrect http:// URL for the subsequent PUT request, the server (correctly) rejects it with a 400 Bad Request and the error Client sent an HTTP request to an HTTPS server. This behavior did not exist in v1.52.3, where the Location header correctly respected the https:// scheme from the `-external-url` flag. Below is a sample (redacted) request/response I extracted. Request (note: https) ``` URI: POST **https**://localhost:4443/upload/storage/v1/b/xxx/o?uploadType=resumable Headers: X-Upload-Content-Type: application/json X-Upload-Content-Length: 4382 User-Agent: google-api-dotnet-client/1.72.0.0 (gzip) Body: xxxx ``` Response (note: http) ``` Status Code: 200 OK Headers: Location: **http**://localhost:4443/upload/storage/v1/b/xxx/o?uploadType=resumable&name=xxx&upload_id=xxx Date: Mon, 02 Feb 2026 17:52:28 GMT Body: {"kind":"storage#object","name":"xxx","id":"xxx","bucket":"xxx","size":"0","contentType":"application/json","acl":[{"bucket":"xxx","entity":"projectOwner-xxx","etag":"RVRhZw==","kind":"storage#objectAccessControl","object":"xxx","projectTeam":{},"role":"OWNER"}],"storageClass":"STANDARD","generation":"0","selfLink":"**http**://localhost:4443/storage/v1/b/xxx/o/xxx","mediaLink":"**http**://localhost:4443/download/storage/v1/b/xxx/o/markets.json?alt=media","metageneration":"1"} ``` --- ## Root cause I think this is due to the new urlhelper.GetBaseURL function attempting to determine the scheme from the request. However, for a direct HTTPS connection (without a reverse proxy), the X-Forwarded-Proto header is not present, and the r.URL.Scheme field on the incoming http.Request is empty. This causes the helper to fall back to a hardcoded "http" scheme, which is incorrect. I'm not a Go developer so I'm not able to suggest a fix myself... Gemini has helped me with this though - do with it what you will :P ``` func getScheme(r *http.Request) string { if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" { return proto } // Add this check if r.TLS != nil { return "https" } if r.URL.Scheme != "" { return r.URL.Scheme } return "http" } ``` Thank you for your work on this project!
kerem 2026-03-03 12:09:27 +03:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@fsouza commented on GitHub (Feb 4, 2026):

Thank you for reporting!

<!-- gh-comment-id:3845598095 --> @fsouza commented on GitHub (Feb 4, 2026): Thank you for reporting!
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/fake-gcs-server#254
No description provided.