Skip to content

Commit 2c215eb

Browse files
committed
fix: aws sdk v3 needs the bucket region to be configured
1 parent 832b1d1 commit 2c215eb

File tree

5 files changed

+69
-28
lines changed

5 files changed

+69
-28
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module.exports = {
99
ecmaVersion: 2020,
1010
},
1111
globals: {
12+
lastS3ClientOptions: 'writable',
1213
mockS3Send: 'writable',
1314
},
1415
rules: {

index.js

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
const animated = require('animated-gif-detector');
44
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
55
const sharp = require('sharp');
6-
const s3Client = new S3Client();
76

87
/**
98
* AWS Lambda function that processes images from S3 based on query parameters.
@@ -23,16 +22,15 @@ exports.handler = async (event, context, callback) => {
2322
return callback(null, response);
2423
}
2524

26-
const bucketDetails = extractBucketDetails(request);
25+
const bucketInfo = extractBucketInfo(request);
2726

28-
if (!bucketDetails) {
27+
if (!bucketInfo) {
2928
return callback(null, response);
3029
}
3130

3231
const allowedContentTypes = ['image/gif', 'image/jpeg', 'image/png'];
33-
const bucket = bucketDetails;
3432
const key = decodeURIComponent(request.uri.substring(1));
35-
const objectResponse = await fetchOriginalImageFromS3(bucket, key);
33+
const objectResponse = await fetchOriginalImageFromS3(bucketInfo, key);
3634

3735
if (!objectResponse.ContentType || !allowedContentTypes.includes(objectResponse.ContentType)) {
3836
return callback(null, response);
@@ -54,7 +52,7 @@ exports.handler = async (event, context, callback) => {
5452
const image = sharp(objectBody);
5553

5654
if (!preserveOriginalFormat) {
57-
contentType = await processImageFormat(image, objectResponse, request, params, formatParam);
55+
contentType = processImageFormat(image, objectResponse, request, params, formatParam);
5856
}
5957

6058
if (params.has('width') || params.has('height')) {
@@ -92,30 +90,28 @@ function isCloudFrontRequestValid(request) {
9290
}
9391

9492
/**
95-
* Extracts the S3 bucket name from the request's origin domain name.
93+
* Extracts the S3 bucket info from the request's origin domain name.
9694
*
9795
* @param {Object} request - CloudFront request object
98-
* @returns {string|null} The bucket name or null if not found
96+
* @returns {Object|null} The bucket info or null if not valid
9997
*/
100-
function extractBucketDetails(request) {
101-
const match = request.origin.s3.domainName.match(/([^.]*)\.s3(\.[^.]*)?\.amazonaws\.com/i);
98+
function extractBucketInfo(request) {
99+
const match = request.origin.s3.domainName.match(/([^.]*)\.s3\.([^.]*)\.amazonaws\.com/i);
102100

103-
if (!match || !match[1] || 'string' !== typeof match[1]) {
104-
return null;
105-
}
106-
107-
return match[1];
101+
return !match ? null : { name: match[1], region: match[2] };
108102
}
109103

110104
/**
111105
* Fetches the original image from S3.
112106
*
113-
* @param {string} bucket - S3 bucket name
107+
* @param {Object} bucketInfo - Object containing bucket name and region
114108
* @param {string} key - S3 object key
115109
* @returns {Promise<Object>} Promise resolving to the S3 object
116110
*/
117-
function fetchOriginalImageFromS3(bucket, key) {
118-
const getObjectCommand = new GetObjectCommand({ Bucket: bucket, Key: key });
111+
function fetchOriginalImageFromS3(bucketInfo, key) {
112+
const s3Client = new S3Client({ region: bucketInfo.region });
113+
const getObjectCommand = new GetObjectCommand({ Bucket: bucketInfo.name, Key: key });
114+
119115
return s3Client.send(getObjectCommand);
120116
}
121117

tests/image-processing-integration.test.js

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ describe('Image Processing Integration', () => {
9797
});
9898

9999
test('should process a simple JPEG without transformations', async () => {
100-
const event = createCloudFrontEvent({ uri: '/test-image.jpg' });
100+
const event = createCloudFrontEvent({
101+
uri: '/test-image.jpg',
102+
region: 'us-west-2',
103+
});
101104

102105
const callback = jest.fn();
103106

@@ -117,12 +120,15 @@ describe('Image Processing Integration', () => {
117120
expect(metadata.format).toBe('jpeg');
118121
expect(metadata.width).toBe(300);
119122
expect(metadata.height).toBe(200);
123+
124+
expect(lastS3ClientOptions.region).toBe('us-west-2');
120125
});
121126

122127
test('should resize JPEG image with width parameter', async () => {
123128
const event = createCloudFrontEvent({
124129
uri: '/test-image.jpg',
125130
querystring: 'width=150',
131+
region: 'us-west-2',
126132
});
127133

128134
const callback = jest.fn();
@@ -142,6 +148,8 @@ describe('Image Processing Integration', () => {
142148
expect(metadata.format).toBe('jpeg');
143149
expect(metadata.width).toBe(150);
144150
expect(metadata.height).toBe(100);
151+
152+
expect(lastS3ClientOptions.region).toBe('us-west-2');
145153
});
146154

147155
test('should convert to WebP when Accept header includes webp', async () => {
@@ -150,6 +158,7 @@ describe('Image Processing Integration', () => {
150158
headers: {
151159
accept: [{ key: 'Accept', value: 'image/webp,image/*' }],
152160
},
161+
region: 'us-west-2',
153162
});
154163

155164
const callback = jest.fn();
@@ -173,11 +182,14 @@ describe('Image Processing Integration', () => {
173182
expect(metadata.format).toBe('webp');
174183
expect(metadata.width).toBe(300);
175184
expect(metadata.height).toBe(200);
185+
186+
expect(lastS3ClientOptions.region).toBe('us-west-2');
176187
});
177188

178189
test('should convert GIF to PNG format', async () => {
179190
const event = createCloudFrontEvent({
180191
uri: '/test-image.gif',
192+
region: 'us-west-2',
181193
});
182194

183195
const callback = jest.fn();
@@ -199,6 +211,8 @@ describe('Image Processing Integration', () => {
199211
expect(metadata.format).toBe('png');
200212
expect(metadata.width).toBe(200);
201213
expect(metadata.height).toBe(200);
214+
215+
expect(lastS3ClientOptions.region).toBe('us-west-2');
202216
});
203217

204218
test('should apply custom quality when parameter is provided', async () => {
@@ -208,6 +222,7 @@ describe('Image Processing Integration', () => {
208222
headers: {
209223
accept: [{ key: 'Accept', value: 'image/webp,image/*' }],
210224
},
225+
region: 'us-west-2',
211226
});
212227

213228
const callback = jest.fn();
@@ -227,6 +242,8 @@ describe('Image Processing Integration', () => {
227242
expect(metadata.format).toBe('webp');
228243
expect(metadata.width).toBe(300);
229244
expect(metadata.height).toBe(200);
245+
246+
expect(lastS3ClientOptions.region).toBe('us-west-2');
230247
});
231248

232249
test('should combine multiple transformations', async () => {
@@ -236,6 +253,7 @@ describe('Image Processing Integration', () => {
236253
headers: {
237254
accept: [{ key: 'Accept', value: 'image/webp,image/*' }],
238255
},
256+
region: 'us-west-2',
239257
});
240258

241259
const callback = jest.fn();
@@ -255,10 +273,15 @@ describe('Image Processing Integration', () => {
255273
expect(metadata.format).toBe('webp');
256274
expect(metadata.width).toBe(250);
257275
expect(metadata.height).toBe(Math.round(200 * (250 / 300)));
276+
277+
expect(lastS3ClientOptions.region).toBe('us-west-2');
258278
});
259279

260280
test('should not process animated GIFs', async () => {
261-
const event = createCloudFrontEvent({ uri: '/animated.gif' });
281+
const event = createCloudFrontEvent({
282+
uri: '/animated.gif',
283+
region: 'us-west-2',
284+
});
262285
const originalResponse = event.Records[0].cf.response;
263286

264287
const callback = jest.fn();
@@ -268,12 +291,15 @@ describe('Image Processing Integration', () => {
268291

269292
expect(callback.mock.calls[0][1]).toBe(originalResponse);
270293
expect(callback.mock.calls[0][1].bodyEncoding).toBeUndefined();
294+
295+
expect(lastS3ClientOptions.region).toBe('us-west-2');
271296
});
272297

273298
test('should not process a GIF when format=original is specified', async () => {
274299
const event = createCloudFrontEvent({
275300
uri: '/test-image.gif',
276301
querystring: 'format=original',
302+
region: 'us-west-2',
277303
});
278304
const originalResponse = event.Records[0].cf.response;
279305

@@ -284,6 +310,8 @@ describe('Image Processing Integration', () => {
284310

285311
expect(callback.mock.calls[0][1]).toBe(originalResponse);
286312
expect(callback.mock.calls[0][1].bodyEncoding).toBeUndefined();
313+
314+
expect(lastS3ClientOptions.region).toBe('us-west-2');
287315
});
288316

289317
test('should preserve original JPEG format when format=original is specified, even with WebP Accept header', async () => {
@@ -293,6 +321,7 @@ describe('Image Processing Integration', () => {
293321
headers: {
294322
accept: [{ key: 'Accept', value: 'image/webp,image/*' }],
295323
},
324+
region: 'us-west-2',
296325
});
297326

298327
const callback = jest.fn();
@@ -313,6 +342,8 @@ describe('Image Processing Integration', () => {
313342
const metadata = await sharp(responseBuffer).metadata();
314343

315344
expect(metadata.format).toBe('jpeg');
345+
346+
expect(lastS3ClientOptions.region).toBe('us-west-2');
316347
});
317348

318349
test('should preserve original JPEG format when format=original is specified with resize parameters, even with WebP Accept header', async () => {
@@ -322,6 +353,7 @@ describe('Image Processing Integration', () => {
322353
headers: {
323354
accept: [{ key: 'Accept', value: 'image/webp,image/*' }],
324355
},
356+
region: 'us-west-2',
325357
});
326358

327359
const callback = jest.fn();
@@ -344,12 +376,15 @@ describe('Image Processing Integration', () => {
344376
expect(metadata.format).toBe('jpeg');
345377
expect(metadata.width).toBe(150);
346378
expect(metadata.height).toBe(100);
379+
380+
expect(lastS3ClientOptions.region).toBe('us-west-2');
347381
});
348382

349383
test('should force conversion to specific format when explicitly requested', async () => {
350384
const event = createCloudFrontEvent({
351385
uri: '/test-image.jpg',
352386
querystring: 'format=webp',
387+
region: 'us-west-2',
353388
});
354389

355390
const callback = jest.fn();
@@ -367,5 +402,7 @@ describe('Image Processing Integration', () => {
367402
const metadata = await sharp(responseBuffer).metadata();
368403

369404
expect(metadata.format).toBe('webp');
405+
406+
expect(lastS3ClientOptions.region).toBe('us-west-2');
370407
});
371408
});

tests/setup.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@ global.mockS3Send = jest.fn();
44

55
jest.mock('@aws-sdk/client-s3', () => {
66
return {
7-
S3Client: jest.fn().mockImplementation(() => ({
8-
send: global.mockS3Send,
9-
})),
7+
S3Client: jest.fn().mockImplementation((options) => {
8+
global.lastS3ClientOptions = options;
9+
10+
return {
11+
send: global.mockS3Send,
12+
};
13+
}),
1014
GetObjectCommand: jest.fn().mockImplementation((params) => ({
1115
...params,
1216
constructor: { name: 'GetObjectCommand' },
@@ -16,4 +20,6 @@ jest.mock('@aws-sdk/client-s3', () => {
1620

1721
beforeEach(() => {
1822
jest.clearAllMocks();
23+
24+
global.lastS3ClientOptions = null;
1925
});

tests/utils.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ const path = require('path');
1010
* @return {Object} CloudFront event data
1111
*/
1212
function createCloudFrontEvent(options = {}) {
13-
const uri = options.uri || '/test-image.jpg';
14-
const querystring = options.querystring || '';
13+
const bucket = options.bucket || 'test-bucket';
1514
const headers = options.headers || {};
15+
const querystring = options.querystring || '';
16+
const region = options.region || 'us-east-1';
1617
const status = options.status || '200';
17-
const bucket = options.bucket || 'test-bucket';
18+
const uri = options.uri || '/test-image.jpg';
1819

1920
return {
2021
Records: [
@@ -26,12 +27,12 @@ function createCloudFrontEvent(options = {}) {
2627
headers,
2728
origin: {
2829
s3: {
29-
domainName: `${bucket}.s3.amazonaws.com`,
30+
domainName: `${bucket}.s3.${region}.amazonaws.com`,
3031
authMethod: 'none',
3132
path: '',
3233
port: 443,
3334
protocol: 'https',
34-
region: 'us-east-1',
35+
region: region,
3536
},
3637
},
3738
},

0 commit comments

Comments
 (0)