😑

[AWS] Serverless Framework + Express で binary を返す

2022/06/28に公開

てこずってしまったのでメモ。

TL;DR

serverless() のオプションに binary として扱いたいタイプの配列を渡せばよい。

import serverless from 'serverless-http'
// 略...
module.exports.handler = serverless(app, { binary: ['image/*'] })

環境

Serverless Framework のExpress 用テンプレートをもとに開始。
Typescript 関連も追加。

$ serverless install --url https://github.com/serverless/examples/tree/v3/aws-node-express-api
$ npm install --save-dev @types/aws-lambda serverless-plugin-typescript 
# 略
serverless.yml
# 略...
functions:
  api:
    handler: index.handler
    events:
      - httpApi: '*'

plugins:
  - serverless-domain-manager
  - serverless-plugin-typescript

custom:
  customDomain:
    domainName: my-domain.com
    basePath: 
    certificateName: my-domain.com
    createRoute53Record: true
    endpointType: 'regional'
    apiType: http
    securityPolicy: tls_1_2
# 略...

S3 から GetObject で .png を取ってきてそのままクライアントに渡すのがやりたかった。

index.ts
// 略...
  async getPng(request: Request, response: Response, next: NextFunction) {
    const filename = `test.png`
    const s3 = new S3Client({ region: 'ap-northeast-1', })
    const output = await s3.send(
      new GetObjectCommand({
        Bucket: 'my-bucket',
        Key: `${filename}`,
        ResponseContentType: 'image/png',
      })
    )
    const readable = output.Body as Readable
    response.setHeader('Content-Type', 'image/png')
    response.setHeader('Content-disposition', `attachment;filename=${filename}`)
    response.setHeader('Content-Length', output.ContentLength as number)
    const chunks = []
    for await (let chunk of readable) {
      chunks.push(chunk)
    }
    const buf = Buffer.concat(chunks)
    return buf
  }
// 略...

問題

test.png は見かけ上、無事ダウンロード。しかし、表示できない、壊れている?
バイナリエディタで確認。

表示できなかった test.png

S3から直接取ってきた test.png

表示できないファイルの先頭が EF BF BD になってしまっている。Google 先生曰く、エンコードの問題?
とにかく、クライアントに届くまでに何らかの変換が行われてしまっている。
buf をそのまま生のバイナリとして送ってほしいのだけど。。。

試行錯誤

charset 指定してみる?

response.setHeader('Content-Type', 'image/png; charset=UTF-8')

だめ。著変無し。

REST API みたく json で返してみる?

return {
  statusCode: 200,
  body: buf.toString('base64'),
  isBase64Encoded: true
}

だめ。そのままのjsonがクライアントに渡るだけ。

pipe してみる?

readable.pipe(response)

だめ。著変無し。

binary 明示して返してみる?

response.end(buf, 'binary')

だめ。著変無し。

ArrayBuffer 返してみる?

return buf.buffer

だめ。なんか変なのが返された。

Serverless のプラグイン使ってみる?

$ npm install --save-dev serverless-plugin-custom-binary
serverless.yml
# 略...
plugins:
  - serverless-domain-manager
  - serverless-plugin-typescript
  - serverless-plugin-custom-binary

custom:
  apigatewayBinary:
    types:
      - image/*
# 略...

だめ。著変無し。(HTTP API 用ではないので当たり前)

成功!

serverless-http のドキュメントに書いてあった。。。
serverless-http | Advanced Options
serverless() のオプションに binary として扱いたいタイプの配列を渡せばよい。

module.exports.handler = serverless(app, { binary: ['image/*'] })

Take home message

公式ドキュメント大切。

Discussion