Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/remove-blob-use-cache.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vercel/blob': minor
---

Deprecate the `useCache` option on `get()`. The backend no longer honors the `cache=0` query parameter it produced, so the option is now a no-op — reads always go through the standard caching path. The option is still accepted (and ignored) to avoid breaking existing callers, and will be removed in a future major version.
21 changes: 4 additions & 17 deletions packages/blob/src/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ export interface GetCommandOptions extends BlobCommandOptions {
*/
access: BlobAccessType;
/**
* Whether to allow the blob to be served from CDN cache.
* When false, fetches directly from origin storage.
* Only effective for private blobs (ignored for public blobs).
* @defaultValue true
* @deprecated No longer has any effect. The backend no longer supports
* bypassing the cache, so this option is ignored. It is kept only to avoid
* breaking existing callers and will be removed in a future major version.
*/
useCache?: boolean;
/**
Expand Down Expand Up @@ -100,17 +99,13 @@ function extractPathnameFromUrl(url: string): string {
* ```ts
* // Basic usage
* const { stream, headers, blob } = await get('user123/avatar.png', { access: 'private' });
*
* // Bypass cache for private blobs (always fetch fresh from storage)
* const { stream, headers, blob } = await get('user123/data.json', { access: 'private', useCache: false });
* ```
*
* Detailed documentation can be found here: https://vercel.com/docs/vercel-blob/using-blob-sdk
*
* @param urlOrPathname - The URL or pathname of the blob to fetch.
* @param options - Configuration options including:
* - access - (Required) Must be 'public' or 'private'. Determines the access level of the blob.
* - useCache - (Optional) When false, fetches directly from origin storage instead of CDN cache. Only effective for private blobs. Defaults to true.
* - oidcToken - (Optional) Vercel OIDC token for authentication with `storeId` (or `BLOB_STORE_ID`); overrides `VERCEL_OIDC_TOKEN`.
* - storeId - (Optional) Store id when using Vercel OIDC token for authentication; overrides `BLOB_STORE_ID`.
* - token - (Optional) Read-write token when not using Vercel OIDC token for authentication, or set `BLOB_READ_WRITE_TOKEN`.
Expand Down Expand Up @@ -177,15 +172,7 @@ export async function get(
...options.headers, // low-level escape hatch, applied last to override anything
};

// Construct fetch URL with optional cache bypass
let fetchUrl = blobUrl;
if (options.useCache === false) {
const url = new URL(blobUrl);
url.searchParams.set('cache', '0');
fetchUrl = url.toString();
}

const response = await fetch(fetchUrl, {
const response = await fetch(blobUrl, {
method: 'GET',
headers: requestHeaders,
signal: options.abortSignal,
Expand Down
161 changes: 0 additions & 161 deletions packages/blob/src/index.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1517,167 +1517,6 @@ describe('blob client', () => {
expect(result?.blob.pathname).toEqual('file.txt');
});

describe('useCache option', () => {
// undici normalizes hostnames to lowercase
const BLOB_STORE_BASE_URL_LOWERCASE =
'https://12345fakestoreid.public.blob.vercel-storage.com';
const BLOB_STORE_BASE_URL_STOREID_LOWERCASE =
'https://storeid.public.blob.vercel-storage.com';

it('should append cache=0 to fetch URL when useCache is false', async () => {
const mockAgent = new MockAgent();
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
const blobStoreMockClient = mockAgent.get(
BLOB_STORE_BASE_URL_LOWERCASE,
);

// Mock the exact path with cache=0 query param
blobStoreMockClient
.intercept({
path: '/foo.txt?cache=0',
method: 'GET',
})
.reply(200, 'blob content', {
headers: {
'content-type': 'text/plain',
'content-length': '12',
},
});

const result = await get('foo.txt', {
access: 'public',
useCache: false,
});

// If the path didn't include ?cache=0, the mock wouldn't match and test would fail
expect(result).not.toBeNull();
expect(result?.blob.pathname).toEqual('foo.txt');
});

it('should not append cache=0 when useCache is true', async () => {
const mockAgent = new MockAgent();
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
const blobStoreMockClient = mockAgent.get(
BLOB_STORE_BASE_URL_LOWERCASE,
);

// Mock the exact path without cache=0
blobStoreMockClient
.intercept({
path: '/foo.txt',
method: 'GET',
})
.reply(200, 'blob content', {
headers: {
'content-type': 'text/plain',
'content-length': '12',
},
});

const result = await get('foo.txt', {
access: 'public',
useCache: true,
});

// If the path included ?cache=0, the mock wouldn't match and test would fail
expect(result).not.toBeNull();
expect(result?.blob.pathname).toEqual('foo.txt');
});

it('should not append cache=0 when useCache is omitted', async () => {
const mockAgent = new MockAgent();
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
const blobStoreMockClient = mockAgent.get(
BLOB_STORE_BASE_URL_LOWERCASE,
);

// Mock the exact path without cache=0
blobStoreMockClient
.intercept({
path: '/foo.txt',
method: 'GET',
})
.reply(200, 'blob content', {
headers: {
'content-type': 'text/plain',
'content-length': '12',
},
});

const result = await get('foo.txt', {
access: 'public',
});

// If the path included ?cache=0, the mock wouldn't match and test would fail
expect(result).not.toBeNull();
expect(result?.blob.pathname).toEqual('foo.txt');
});

it('should not include cache=0 in returned downloadUrl', async () => {
const mockAgent = new MockAgent();
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
const blobStoreMockClient = mockAgent.get(
BLOB_STORE_BASE_URL_LOWERCASE,
);

blobStoreMockClient
.intercept({
path: '/foo.txt?cache=0',
method: 'GET',
})
.reply(200, 'blob content', {
headers: {
'content-type': 'text/plain',
'content-length': '12',
},
});

const result = await get('foo.txt', {
access: 'public',
useCache: false,
});

expect(result?.blob.downloadUrl).toEqual(
'https://12345fakestoreid.public.blob.vercel-storage.com/foo.txt?download=1',
);
expect(result?.blob.url).toEqual(
'https://12345fakeStoreId.public.blob.vercel-storage.com/foo.txt',
);
});

it('should work with URL input and useCache false', async () => {
const mockAgent = new MockAgent();
mockAgent.disableNetConnect();
setGlobalDispatcher(mockAgent);
// Use lowercase since undici normalizes hostnames
const storeMock = mockAgent.get(BLOB_STORE_BASE_URL_STOREID_LOWERCASE);

storeMock
.intercept({
path: '/foo.txt?cache=0',
method: 'GET',
})
.reply(200, 'blob content', {
headers: {
'content-type': 'text/plain',
'content-length': '12',
},
});

const result = await get(`${BLOB_STORE_BASE_URL}/foo.txt`, {
access: 'public',
useCache: false,
});

// If the path didn't include ?cache=0, the mock wouldn't match and test would fail
expect(result).not.toBeNull();
});
});

describe('conditional requests (304 Not Modified)', () => {
const BLOB_STORE_BASE_URL_LOWERCASE =
'https://12345fakestoreid.private.blob.vercel-storage.com';
Expand Down
Loading