CreateSignedUrl fails when object content-type is different from "application/json"
Bug report
Describe the bug
CreateSignedUrl fails with body must be object when target object content-type is different from "application/json"
To Reproduce
// Upload file and return signed url
func uploadFile() (string, error) {
path := "data/file.txt"
content := "Hello, I'm a file"
// Upload OK
fileRes, _ := supaStorage.UploadFile(SUPA_BUCKET_NAME, path, strings.NewReader(content), supa_storage.FileOptions{
Upsert: makePointer(true),
ContentType: makePointer("text/plain"), // <-- This line causes the error on the next method call
})
urlRes, err := supaStorage.CreateSignedUrl(SUPA_BUCKET_NAME, path, 60 * 30)
if err != nil {
log.Default().Println(err) // "body must be object" when setting content type
return "", err
}
return urlRes.SignedURL, nil
}
If you do not set ContentType, or set it to the default "application/json" it works.
fileRes, _ := supaStorage.UploadFile(SUPA_BUCKET_NAME, path, strings.NewReader(content), supa_storage.FileOptions{
Upsert: makePointer(true),
// ContentType: makePointer("text/plain")
})
urlRes, err := supaStorage.CreateSignedUrl(SUPA_BUCKET_NAME, path, 60 * 30)
// No error
if err != nil {
log.Default().Println(err)
return "", err
}
Expected behavior
Method should return the signed url regardless of content type
System information
- github.com/supabase-community/storage-go v0.7.0
Additional context
After some digging, the error seems to be coming from the execution of the request at storage.go@CreateSignedUrl
// storage.go
// CreateSignedUrl create a signed URL. Use a signed URL to share a file for a fixed amount of time.
// bucketId string The bucket id
// filePath path The file path, including the file name. Should be of the format `folder/subfolder/filename.png`
// expiresIn int The number of seconds before the signed URL expires. Defaults to 60 seconds.
func (c *Client) CreateSignedUrl(bucketId string, filePath string, expiresIn int) (SignedUrlResponse, error) {
signedURL := c.clientTransport.baseUrl.String() + "/object/sign/" + bucketId + "/" + filePath
jsonBody := map[string]interface{}{
"expiresIn": expiresIn,
}
req, err := c.NewRequest(http.MethodPost, signedURL, &jsonBody)
if err != nil {
return SignedUrlResponse{}, err
}
var response SignedUrlResponse
_, err = c.Do(req, &response)
if err != nil { // <---- This is the error being triggered
return SignedUrlResponse{}, err
}
response.SignedURL = c.clientTransport.baseUrl.String() + response.SignedURL
return response, nil
}
However, I'm really new to Golang and don't really know what could be causing this. Maybe there's an option to parse non-JSON bodies differently? Also, this is not on Supabase's side, as the NodeJS client seems to do this just fine
I think I found the problem.
When you call UploadOrUpdateFile with options, the options permanently affect the headers of all request:
// storage.go
func (c *Client) UploadOrUpdateFile(
bucketId string,
relativePath string,
data io.Reader,
update bool,
options ...FileOptions,
) (FileUploadResponse, error) {
path := removeEmptyFolderName(bucketId + "/" + relativePath)
uploadURL := c.clientTransport.baseUrl.String() + "/object/" + path
// Check on file options
if len(options) > 0 {
if options[0].CacheControl != nil {
c.clientTransport.header.Set("cache-control", *options[0].CacheControl)
}
if options[0].ContentType != nil {
c.clientTransport.header.Set("content-type", *options[0].ContentType) // <-- content-type is set to whatever you put
}
if options[0].Upsert != nil {
c.clientTransport.header.Set("x-upsert", strconv.FormatBool(*options[0].Upsert))
}
}
This header doesn't get reset back to "application/json", so when you fire the next request (in my case to generate a signed URL to the object) it gets sent with the header you just set. You can see this dumping the request, responses:
// Here I'm uploading a file with the default content type
REQUEST:
%s POST /storage/v1/object/otc-data/orders/20583240451453857792/chat.txt HTTP/1.1
Host: rqvnhdsauyuopdhfgoou.supabase.co
Accept: application/json
Authorization: Bearer xxxxxxx
Content-Type: application/json // <-- unchanged
X-Client-Info: storage-go/v0.7.0
X-Upsert: true
2024/01/24 18:15:34 RESPONSE:
%s HTTP/2.0 200 OK
Access-Control-Allow-Origin: *
Alt-Svc: h3=":443"; ma=86400
Cf-Cache-Status: DYNAMIC
Cf-Ray: 84abfaac5a1a226f-MIA
Content-Type: application/json; charset=utf-8
Date: Wed, 24 Jan 2024 23:15:34 GMT
Sb-Gateway-Mode: direct
Sb-Gateway-Version: 1
Server: cloudflare
Strict-Transport-Security: max-age=2592000; includeSubDomains
Vary: Accept-Encoding
{"Id":"ad7b3555-8b16-4428-aa1c-9cbb9a3f82df","Key":"otc-data/orders/20583240451453857792/chat.txt"}
// And then generating a signed URL
2024/01/24 18:15:34 REQUEST:
%s POST /storage/v1/object/sign/otc-data/orders/20583240451453857792/chat.txt HTTP/1.1
Host: rqvnhdsauyuopdhfgoou.supabase.co
Accept: application/json
Authorization: Bearer xxxx
Content-Type: application/json // <-- unchanged
X-Client-Info: storage-go/v0.7.0
X-Upsert: true
2024/01/24 18:15:34 RESPONSE:
%s HTTP/2.0 200 OK
Access-Control-Allow-Origin: *
Alt-Svc: h3=":443"; ma=86400
Cf-Cache-Status: DYNAMIC
Cf-Ray: 84abfaaedddd226f-MIA
Content-Type: application/json; charset=utf-8
Date: Wed, 24 Jan 2024 23:15:34 GMT
Sb-Gateway-Mode: direct
Sb-Gateway-Version: 1
Server: cloudflare
Strict-Transport-Security: max-age=2592000; includeSubDomains
Vary: Accept-Encoding
{"signedURL":"/object/sign/otc-data/orders/20583240451453857792/chat.txt?token=xxxxxx"}
This happens when you set the content-type
2024/01/24 18:15:35 REQUEST:
%s POST /storage/v1/object/otc-data/orders/20583240451453857792/0.jpg HTTP/1.1
Host: rqvnhdsauyuopdhfgoou.supabase.co
Accept: application/json
Authorization: Bearer xxxx
Content-Type: text/plain // <-- THIS CHANGED TO MATCH FILE
X-Client-Info: storage-go/v0.7.0
X-Upsert: true
// First response is OK
2024/01/24 18:15:35 RESPONSE:
%s HTTP/2.0 200 OK
Access-Control-Allow-Origin: *
Alt-Svc: h3=":443"; ma=86400
Cf-Cache-Status: DYNAMIC
Cf-Ray: 84abfab22a44226f-MIA
Content-Type: application/json; charset=utf-8
Date: Wed, 24 Jan 2024 23:15:36 GMT
Sb-Gateway-Mode: direct
Sb-Gateway-Version: 1
Server: cloudflare
Strict-Transport-Security: max-age=2592000; includeSubDomains
Vary: Accept-Encoding
{"Id":"6d488ac9-0472-4449-9e5a-f843199de539","Key":"otc-data/orders/20583240451453857792/0.jpg"}
2024/01/24 18:15:35 {otc-data/orders/20583240451453857792/0.jpg [] }
2024/01/24 18:15:35
// But when you try to generate the signed URL
2024/01/24 18:15:35 REQUEST:
%s POST /storage/v1/object/sign/otc-data/orders/20583240451453857792/0.jpg HTTP/1.1
Host: rqvnhdsauyuopdhfgoou.supabase.co
Accept: application/json
Authorization: Bearer xxxx
Content-Type: text/plain // <-- and DID NOT reset
X-Client-Info: storage-go/v0.7.0
X-Upsert: true
// So Supabase says something is wrong with your request body
RESPONSE:
%s HTTP/2.0 400 Bad Request
Content-Length: 68
Access-Control-Allow-Origin: *
Alt-Svc: h3=":443"; ma=86400
Cf-Cache-Status: DYNAMIC
Cf-Ray: 84abfab6f975226f-MIA
Content-Type: application/json; charset=utf-8
Date: Wed, 24 Jan 2024 23:15:36 GMT
Sb-Gateway-Mode: direct
Sb-Gateway-Version: 1
Server: cloudflare
Strict-Transport-Security: max-age=2592000; includeSubDomains
Vary: Accept-Encoding
{"statusCode":"400","error":"Error","message":"body must be object"}
If you add c.clientTransport.header.Set("content-type", "application/json") right after you fire UploadFile request, the subsequent request work as expected.