Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
bjuan210302 opened this issue Jan 24, 2024 · 2 comments
Labels
bug Something isn't working

Comments

@bjuan210302
Copy link
Contributor

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 bjuan210302 added the bug Something isn't working label Jan 24, 2024
@bjuan210302
Copy link
Contributor Author

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.

@geffersonFerraz
Copy link

Hello @tranhoangvuit , this is an important fix, and not wanting to be a bother by asking, do we have plans for a new release?"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants