storage-go icon indicating copy to clipboard operation
storage-go copied to clipboard

CreateSignedUrl fails when object content-type is different from "application/json"

Open bjuan210302 opened this issue 2 years ago • 4 comments

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

bjuan210302 avatar Jan 24 '24 22:01 bjuan210302

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.

bjuan210302 avatar Jan 24 '24 23:01 bjuan210302