Skip to content

Commit

Permalink
Enable awsjson property for encoding a payload (or specific payload…
Browse files Browse the repository at this point in the history
… properties)

Enable individual requests to override host, port, protocol
Ensure proper host, port, protocol are tagged to connection errors
Parse errors as regular JSON, even when they are AWS JSON for some reason
Break out AWS JSON testing into its own block
Tidy up tests a wee bit
  • Loading branch information
ryanblock committed Sep 19, 2023
1 parent 2e7f326 commit cf0a1dc
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 79 deletions.
93 changes: 65 additions & 28 deletions src/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,25 @@ let { is } = require('./validate')
let { useAWS } = require('./lib')
let { marshall: marshallAwsJSON, unmarshall: unmarshallAwsJSON } = require('./_vendor')

let JSONregex = /application\/json/
let JSONContentType = ct => ct.match(JSONregex)
let AwsJSONregex = /application\/x-amz-json/
let AwsJSONContentType = ct => ct.match(AwsJSONregex)

module.exports = function request (params, creds, region, config, metadata) {
return new Promise((resolve, reject) => {
// Normalize the path + hostname

// Path
params.path = params.endpoint || '/'
if (!params.path.startsWith('/')) {
params.path = '/' + params.path
}
params.host = params.host || params.hostname

// Host
params.host = params.host || params.hostname || config.host || config.hostname
if (params.hostname) delete params.hostname

// Accept structured query string
// Structured query string
if (params.query) {
if (!is.object(params.query)) {
throw ReferenceError('Query property must be an object')
Expand All @@ -24,29 +32,47 @@ module.exports = function request (params, creds, region, config, metadata) {
params.path += '?' + qs.stringify(params.query)
}

// JSON-ify payload where convenient
// Headers, content-type
let headers = params.headers || {}
let contentType = headers['content-type'] || headers['Content-Type'] || ''
/* istanbul ignore next */
if (headers['Content-Type']) delete headers['Content-Type']

// Body - JSON-ify payload where convenient!
let body = params.payload || params.body || params.data || params.json
// Yeah, lots of potentially weird valid json (like just a null), deal with it if/when we need to I guess
let aws_json = params.awsjson || params.AWSJSON
// Lots of potentially weird valid json (like just a null), deal with it if / when we need to I guess
if (typeof body === 'object') {
params.headers = params.headers || {}
if (!params.headers['content-type'] && !params.headers['Content-Type']) {
params.headers['content-type'] = 'application/json'
}
// Normalize AWS JSON headers
if (params.headers['content-type'].includes('application/x-amz-json')) {
params.useAwsJSON = true
}
if (params.useAwsJSON) {
if (!params.headers['content-type'].includes('application/x-amz-json')) {
params.headers['content-type'] = 'application/x-amz-json-1.1'
// Backfill content-type if it's just an object
if (!contentType) contentType = 'application/json'
// A variety of services use AWS JSON; we'll make it easier via a header or passed param
if (AwsJSONContentType(contentType) || aws_json) {
// Backfill content-type header yet again
if (!AwsJSONContentType(contentType)) {
contentType = 'application/x-amz-json-1.0'
}
// We may not be able to AWS JSON-encode the whole payload, check for specified keys
if (Array.isArray(aws_json)) {
body = Object.entries(body).reduce((acc, [ k, v ]) => {
if (aws_json.includes(k)) acc[k] = marshallAwsJSON(v)
else acc[k] = v
return acc
}, {})
}
body = marshallAwsJSON(body)
// Otherwise, just AWS JSON-encode the whole thing
else body = marshallAwsJSON(body)
}
// Final JSON encoding
params.body = JSON.stringify(body)
}
// Everything else just passes through
else params.body = body

// Let aws4 handle (most) logic related to region instantiation
// Finalize headers, content-type
if (contentType) headers['content-type'] = contentType
params.headers = headers

// Sign the payload; let aws4 handle (most) logic related to region + service instantiation
let signing = { region, ...params }
/* istanbul ignore next */
if (globalServices.includes(params.service)) {
Expand All @@ -61,34 +87,45 @@ module.exports = function request (params, creds, region, config, metadata) {

// Sign and construct the request
let options = aws4.sign(signing, creds)
// Renormalize (again), aws4 sometimes uses host, sometimes uses hostname
// Normalize host (again): aws4 sometimes uses host, sometimes hostname
/* istanbul ignore next */ // This won't get seen by nyc
options.host = options.host || options.hostname
/* istanbul ignore next */
if (options.hostname) delete options.hostname

// Importing http(s) is a bit slow (~1ms), so only instantiate the client we need
let isHTTPS = options.host.includes('.amazonaws.com') || config.protocol === 'https'
options.protocol = (params.protocol || config.protocol) + ':'
let isHTTPS = options.host.includes('.amazonaws.com') || options.protocol === 'https:'
/* istanbul ignore next */ // eslint-disable-next-line
let http = isHTTPS ? require('https') : require('http')

// Port configuration
options.port = params.port || config.port

// Disable keep-alive locally (or wait Node's default 5s for sockets to time out)
/* istanbul ignore next */
options.agent = new http.Agent({ keepAlive: config.keepAlive ?? useAWS() })

let req = http.request(options, res => {
let data = []
let { headers, statusCode } = res
/* istanbul ignore next */ // We can always expect headers, but jic
let { headers = {}, statusCode } = res
let ok = statusCode >= 200 && statusCode < 303
res.on('data', chunk => data.push(chunk))
res.on('end', () => {
let result = data.join()
let contentType = headers?.['content-type'] || headers?.['Content-Type'] || ''
if (contentType.includes('application/json')) {
let contentType = headers['content-type'] || headers['Content-Type'] || ''
if (JSONContentType(contentType)) {
result = JSON.parse(result)
}
if (contentType.includes('application/x-amz-json')) {
result = unmarshallAwsJSON(JSON.parse(result))
// Some services may attempt to respond with regular JSON, but an AWS JSON content-type. Sure. Ok. Anyway, try to guard against that.
if (AwsJSONContentType(contentType)) {
try {
result = unmarshallAwsJSON(JSON.parse(result))
}
catch {
result = JSON.parse(result)
}
}
if (ok) resolve(result)
else reject({ error: result, metadata, statusCode })
Expand All @@ -100,9 +137,9 @@ module.exports = function request (params, creds, region, config, metadata) {
...metadata,
rawStack: error.stack,
service: params.service,
host: params.host,
protocol: config.protocol,
port: params.port,
host: options.host,
protocol: options.protocol.replace(':', ''),
port: options.port,
}
}))
req.end(options.body || '')
Expand Down
5 changes: 1 addition & 4 deletions test/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,9 @@ const secretAccessKey = 'bar'
const service = 'lambda'
const endpoint = '/an/endpoint'
const port = 1111
const config = { accessKeyId, secretAccessKey, region, protocol, autoloadPlugins, keepAlive }
const config = { accessKeyId, secretAccessKey, region, protocol, autoloadPlugins, keepAlive, host, port }
const defaults = { accessKeyId, autoloadPlugins, badPort, config, host, keepAlive, protocol, region, secretAccessKey, service, endpoint, port }

const copy = i => JSON.parse(JSON.stringify(i))

let serverData = {}

let testServer
Expand Down Expand Up @@ -107,7 +105,6 @@ function resetAWSEnvVars () {

module.exports = {
basicRequestChecks,
copy,
defaults,
resetAWSEnvVars,
resetServer,
Expand Down
Loading

0 comments on commit cf0a1dc

Please sign in to comment.