Skip to content

Commit c98f14a

Browse files
authored
fix: @aws-sdk/client-s3 instrumentation could cause getSignedUrl to crash (#3467)
Closes: #3464
1 parent 3def330 commit c98f14a

File tree

6 files changed

+129
-13
lines changed

6 files changed

+129
-13
lines changed

CHANGELOG.asciidoc

+3
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ See <<esm>> for full details.
8181
* Fix aws-sdk v3 instrumentation (currently just `@aws-sdk/client-s3`) for
8282
versions 3.363.0 and later. ({pull}3455[#3455])
8383
84+
* Fix a possible crash when using `getSignedUrl()` from `@aws-sdk/s3-request-presigner`
85+
due to a bug in `@aws-sdk/client-s3` instrumentation. ({issues}3464[#3464])
86+
8487
[float]
8588
===== Chores
8689

lib/instrumentation/modules/@aws-sdk/client-s3.js

+10-9
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,15 @@ function s3MiddlewareFactory (client, agent) {
6161
return [
6262
{
6363
middleware: (next, context) => async (args) => {
64-
const input = args.input
65-
const bucket = input && input.Bucket
66-
const resource = resourceFromBucket(bucket)
64+
// Ensure there is a span from the wrapped `client.send()`.
6765
const span = agent._instrumentation.currSpan()
68-
69-
if (!span) {
66+
if (!span || !(span.type === TYPE && span.subtype === SUBTYPE)) {
7067
return await next(args)
7168
}
69+
70+
const input = args.input
71+
const bucket = input && input.Bucket
72+
const resource = resourceFromBucket(bucket)
7273
// The given span comes with the operation name and we need to
7374
// add the resource if applies
7475
if (resource) {
@@ -152,17 +153,18 @@ function s3MiddlewareFactory (client, agent) {
152153

153154
// Destination context.
154155
const destContext = {
155-
address: context[elasticAPMStash].hostname,
156-
port: context[elasticAPMStash].port,
157156
service: {
158157
name: SUBTYPE,
159158
type: TYPE
160159
}
161160
}
161+
if (context[elasticAPMStash]) {
162+
destContext.address = context[elasticAPMStash].hostname
163+
destContext.port = context[elasticAPMStash].port
164+
}
162165
if (resource) {
163166
destContext.service.resource = resource
164167
}
165-
166168
if (region) {
167169
destContext.cloud = { region }
168170
}
@@ -190,7 +192,6 @@ function s3MiddlewareFactory (client, agent) {
190192
}
191193

192194
context[elasticAPMStash] = {
193-
protocol: req.protocol,
194195
hostname: req.hostname,
195196
port
196197
}

package-lock.json

+63
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
"devDependencies": {
124124
"@apollo/server": "^4.2.2",
125125
"@aws-sdk/client-s3": "^3.363.0",
126+
"@aws-sdk/s3-request-presigner": "^3.363.0",
126127
"@babel/cli": "^7.8.4",
127128
"@babel/core": "^7.8.4",
128129
"@babel/preset-env": "^7.8.4",

test/instrumentation/modules/@aws-sdk/client-s3.test.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const testFixtures = [
7777
spans.length, 'all spans have sync=false')
7878
t.equal(spans.filter(s => s.sample_rate === 1).length,
7979
spans.length, 'all spans have sample_rate=1')
80-
const failingSpanId = spans[7].id // index of `getObjNonExistantObject`
80+
const failingSpanId = spans[8].id // index of `getObjNonExistantObject`
8181
spans.forEach(s => {
8282
// Remove variable and common fields to facilitate t.deepEqual below.
8383
delete s.id
@@ -224,6 +224,14 @@ const testFixtures = [
224224
outcome: 'success'
225225
}, 'getObj produced expected span')
226226

227+
t.deepEqual(spans.shift(), {
228+
name: 'get-signed-url',
229+
type: 'custom',
230+
subtype: null,
231+
action: null,
232+
outcome: 'success'
233+
}, 'custom span for getSignedUrl call')
234+
227235
t.deepEqual(spans.shift(), {
228236
name: 'S3 GetObject elasticapmtest-bucket-3',
229237
type: 'storage',

test/instrumentation/modules/@aws-sdk/fixtures/use-client-s3.js

+43-3
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,34 @@ const {
6363
waitUntilBucketExists,
6464
waitUntilObjectExists
6565
} = require('@aws-sdk/client-s3')
66+
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner')
6667

6768
const TEST_BUCKET_NAME_PREFIX = 'elasticapmtest-bucket-'
6869

6970
// ---- support functions
7071

72+
/**
73+
* Slurp everything from the given ReadableStream and return the content,
74+
* converted to a string.
75+
*/
76+
async function slurpStream (stream) {
77+
return new Promise((resolve, reject) => {
78+
const chunks = []
79+
stream.on('error', (err) => {
80+
reject(err)
81+
})
82+
stream.on('readable', function () {
83+
let chunk
84+
while ((chunk = this.read()) !== null) {
85+
chunks.push(chunk)
86+
}
87+
})
88+
stream.on('end', () => {
89+
resolve(Buffer.concat(chunks).toString('utf8'))
90+
})
91+
})
92+
}
93+
7194
// https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/index.html
7295
async function useClientS3 (s3Client, bucketName) {
7396
const region = await s3Client.config.region()
@@ -129,14 +152,30 @@ async function useClientS3 (s3Client, bucketName) {
129152
// https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/classes/getobjectcommand.html
130153
command = new GetObjectCommand({ Bucket: bucketName, Key: key })
131154
data = await s3Client.send(command)
132-
log.info({ data }, 'getObject')
155+
// `data.Body` is a *stream*, so we cannot just log it
156+
// https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/interfaces/getobjectcommandoutput.html#body
157+
const body = await slurpStream(data.Body)
158+
assert(body === content)
159+
delete data.Body
160+
log.info({ data, body }, 'getObject')
161+
162+
// Get a signed URL.
163+
// This is interesting to test, because `getSignedUrl` uses the command
164+
// `middlewareStack` -- including our added middleware -- **without** calling
165+
// `s3Client.send()`. The test here is to ensure this doesn't break.
166+
const customSpan = apm.startSpan('get-signed-url')
167+
const signedUrl = await getSignedUrl(
168+
s3Client,
169+
new GetObjectCommand({ Bucket: bucketName, Key: key }),
170+
{ expiresIn: 3600 })
171+
log.info({ signedUrl }, 'getSignedUrl')
172+
customSpan.end()
133173

134174
command = new GetObjectCommand({
135175
IfNoneMatch: etag,
136176
Bucket: bucketName,
137177
Key: key
138178
})
139-
140179
try {
141180
data = await s3Client.send(command)
142181
throw new Error('expected NotModified error for conditional request')
@@ -228,7 +267,8 @@ function main () {
228267
s3Client.destroy()
229268
process.exitCode = 0
230269
},
231-
function () {
270+
function (err) {
271+
apm.logger.error(err, 'useClientS3 rejected')
232272
tx.setOutcome('failure')
233273
tx.end()
234274
s3Client.destroy()

0 commit comments

Comments
 (0)