Unable to resize photos using getSignedUrls (JS API)
Bug report
- [x] I confirm this is a bug with Supabase, not with my own application.
- [x] I confirm I have searched the Docs, GitHub Discussions, and Discord.
Describe the bug
The createSignedUrls function does not allow image transformations. Very annoying because createSignedUrl works well with image transformations, but is far too slow when each URL needs to make a separate request to the server.
To Reproduce
- Try creating signed URLs with a transform option. Currently the docs do not indicate that it is possible
Expected behavior
It should create signed URLs that apply a certain transform
Screenshots
N/A
Additional context
Apologies if this would be a feature request.
+1 for this. A huge bonus if this could return the original url as well as the transformed as an option.
I am having the same issue. Documentation mentions that this should be possible. https://supabase.com/docs/guides/storage/serving/image-transformations#signing-urls-with-transformation-options
I believe this should be labelled as a bug because it is a documented feature after all. https://supabase.com/docs/guides/storage/serving/image-transformations?queryGroups=language&language=js#signing-urls-with-transformation-options
I've also run into this limitation and would like this added to the client. I did some investigation and looks like this is not technically supported by the server because the transform options are not referenced for getSignedURLs in the Storage server route.
- For
getSignedUrl, transform options are used: https://github.com/supabase/storage/blob/e416b7a09a9ca11667c64b827e671b216d588063/src/http/routes/object/getSignedURL.ts#L75 - For
getSignedUrls, transform options are NOT used: https://github.com/supabase/storage/blob/e416b7a09a9ca11667c64b827e671b216d588063/src/http/routes/object/getSignedURLs.ts#L81
I did a bit more digging to understand how transformation options are applied to the URLs, and it looks like the token passed to the client has a transform field inside of it. Here is an example payload:
{
"url": "bucket/image",
"transformations": "width:500,resize:contain",
"iat": 1724691957,
"exp": 1724699157
}
If you're looking for a workaround that doesn't involve multiple calls to getSignedUrl, you could consider calling getSignedUrls and make the following changes:
- Parse the token returned in the URL
- Add your transformation options in the payload
- Resign the token
- Update the token in your URL with your modified token.
However, at this time it's unclear where to find the key is used to sign these.
I highly advise against doing this as this would break if there's ever a change in the server behaviour or its expected format for the tokens. Use this idea at your own risk.
At the time of writing, there doesn't appear to be any pull requests, issues, or discussions around this missing functionality on the server side. I suspect based on looking at the storage-js and storage codebases that this feature should be trivial to implement. Some places that might be helpful to look at for anyone implementing:
- https://github.com/supabase/storage/blob/e416b7a09a9ca11667c64b827e671b216d588063/src/storage/object.ts#L558
- https://github.com/supabase/storage/blob/e416b7a09a9ca11667c64b827e671b216d588063/src/storage/object.ts#L597C9-L597C23
Is there anyone from the Supabase team who could comment if this is on the roadmap? How would we request this gets added to both the client library and the server implementation?
Do you have a Pro plan or above?
Image Resizing is currently enabled for Pro Plan and above.
https://supabase.com/docs/guides/storage/serving/image-transformations?queryGroups=language&language=js#signing-urls-with-transformation-options
For me it was not working cause I was using the free plan. However, it is confusing that just nothing happens if you are using free plan. Took me quite a while to figure out the freature is disabled for free plan.
Hi everyone - just checking in here, as @nicofunke has said image transformations are for pro plans an above, Are you on pro plans?
Also is their anyway that the docs could be change to highlight the limitations to pro plan and above? If so how?
I am on a pro plan. The limitation of the API is still occurring where I cannot bulk sign URLs and resize images.
+1. I'd also appreciate this
highly advise against doing this as this would break if there's ever a change in the server behaviour or its expected format for the tokens. Use this idea at your own risk.
This shouldn't work. If it does, Supabase has major security issues. As you said you don't have access to the key signing the JWT. Hopefully Supabase verifies the JWT signature before responding with the image. If not, the whole purpose with private buckets is gone
Hi all, I've moved this over to the storage js repo from the supabase repo
Updated to pro plan ending up to find transform doesn't work. Obviously a solution is to pass transform to createSignedUrls, just like createSignedUrl. Current implementation is shown below for further discussion.
/@[email protected]/node_modules/@supabase/storage-js/src/packages/StorageFileApi.ts
/**
* Creates a signed URL. Use a signed URL to share a file for a fixed amount of time.
*
* @param path The file path, including the current file name. For example `folder/image.png`.
* @param expiresIn The number of seconds until the signed URL expires. For example, `60` for a URL which is valid for one minute.
* @param options.download triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename.
* @param options.transform Transform the asset before serving it to the client.
*/
async createSignedUrl(
path: string,
expiresIn: number,
options?: { download?: string | boolean; transform?: TransformOptions }
): Promise<
| {
data: { signedUrl: string }
error: null
}
| {
data: null
error: StorageError
}
> {
try {
let _path = this._getFinalPath(path)
let data = await post(
this.fetch,
`${this.url}/object/sign/${_path}`,
{ expiresIn, ...(options?.transform ? { transform: options.transform } : {}) },
{ headers: this.headers }
)
const downloadQueryParam = options?.download
? `&download=${options.download === true ? '' : options.download}`
: ''
const signedUrl = encodeURI(`${this.url}${data.signedURL}${downloadQueryParam}`)
data = { signedUrl }
return { data, error: null }
} catch (error) {
if (isStorageError(error)) {
return { data: null, error }
}
throw error
}
}
/**
* Creates multiple signed URLs. Use a signed URL to share a file for a fixed amount of time.
*
* @param paths The file paths to be downloaded, including the current file names. For example `['folder/image.png', 'folder2/image2.png']`.
* @param expiresIn The number of seconds until the signed URLs expire. For example, `60` for URLs which are valid for one minute.
* @param options.download triggers the file as a download if set to true. Set this parameter as the name of the file if you want to trigger the download with a different filename.
*/
async createSignedUrls(
paths: string[],
expiresIn: number,
options?: { download: string | boolean }
): Promise<
| {
data: { error: string | null; path: string | null; signedUrl: string }[]
error: null
}
| {
data: null
error: StorageError
}
> {
try {
const data = await post(
this.fetch,
`${this.url}/object/sign/${this.bucketId}`,
{ expiresIn, paths },
{ headers: this.headers }
)
const downloadQueryParam = options?.download
? `&download=${options.download === true ? '' : options.download}`
: ''
return {
data: data.map((datum: { signedURL: string }) => ({
...datum,
signedUrl: datum.signedURL
? encodeURI(`${this.url}${datum.signedURL}${downloadQueryParam}`)
: null,
})),
error: null,
}
} catch (error) {
if (isStorageError(error)) {
return { data: null, error }
}
throw error
}
}