Search code examples
amazon-web-servicesamazon-s3aws-api-gateway

Rendering JPG images from S3 bucket over HTTPS via APIGateway


I have a Cloudformation template which serves S3 text and binary content (mp3, jpeg, png) over HTTPS via API Gateway.

I have a series of hello assets (json, mp3, jpg) which I push to the S3 bucket, being careful to include the correct MIME type for each.

I then go to the browser to see if I can access the assets via HTTPS.

hello.json works fine; I can see {"hello": "world"} in the browser screen.

hello.mp3 also works fine; the browser renders an mp3 player and I can hear "hello world" being spoken.

But hello.jpg renders a broken image :/


My first thought is that the image itself is broken; but I can curl it to a local file and the browser will render the file image fine.

My second thought is that somehow there is some mixup between the browser and API Gateway regarding raw binary vs base64 encoded binary data; I know that API Gateway likes to render b64 data on occasion.

It seems there is indeed some mixup here. The jpeg raw file is 1467 bytes in length, but if I inspect the Chrome Javascript console, the server seems response with a length of more than 1900. I think the server may therefore be returning b64 data as I know b64 files are normally approx 1/3 bigger than the raw files.

So then I think maybe the HTTP request headers are influencing the response type. But no matter what Accept header values I give to curl, the server always returns 1467 Content-Length. I can't get curl to replicate what the browser is doing with the server.

I am also confused as to why the browser would happily render mp3 files, but not jpg files from the same endpoint.

Any thoughts as to how to resolve this? Thank you.

---
Outputs: {}
Parameters:
  AppName:
    Type: String
  DomainName:
    Type: String
  CertificateArn:
    Type: String
  MemorySizeDefault:
    Default: '512'
    Type: String
  RuntimeVersion:
    Default: '3.10'
    Type: String
  TimeoutDefault:
    Default: '5'
    Type: String
Resources:
  MyWebsite:
    Properties:
      BucketName: !Sub "${AppName}-bucket"
    Type: AWS::S3::Bucket
  MyWebsiteDeployment:
    DependsOn:
    - MyWebsiteMethod
    Properties:
      RestApiId:
        Ref: MyWebsiteRestApi
    Type: AWS::ApiGateway::Deployment
  MyWebsiteDomain:
    Properties:
      CertificateArn:
        Ref: CertificateArn
      DomainName:
        Ref: DomainName
    Type: AWS::ApiGateway::DomainName
  MyWebsiteDomainPathMapping:
    DependsOn:
    - MyWebsiteDomain
    Properties:
      DomainName:
        Ref: DomainName
      RestApiId:
        Ref: MyWebsiteRestApi
      Stage: prod
    Type: AWS::ApiGateway::BasePathMapping
  MyWebsiteDomainRecordSet:
    Properties:
      AliasTarget:
        DNSName:
          Fn::GetAtt:
          - MyWebsiteDomain
          - DistributionDomainName
        EvaluateTargetHealth: false
        HostedZoneId:
          Fn::GetAtt:
          - MyWebsiteDomain
          - DistributionHostedZoneId
      HostedZoneName:
        Fn::Sub:
        - ${prefix}.${suffix}.
        - prefix:
            Fn::Select:
            - 1
            - Fn::Split:
              - .
              - Ref: DomainName
          suffix:
            Fn::Select:
            - 2
            - Fn::Split:
              - .
              - Ref: DomainName
      Name:
        Ref: DomainName
      Type: A
    Type: AWS::Route53::RecordSet
  MyWebsiteMethod:
    Properties:
      AuthorizationType: NONE
      HttpMethod: GET
      Integration:
        Credentials:
          Fn::GetAtt:
          - MyWebsiteRole
          - Arn
        IntegrationHttpMethod: ANY
        IntegrationResponses:
        - ResponseParameters:
            method.response.header.Content-Type: integration.response.header.Content-Type
          StatusCode: 200
        - SelectionPattern: '404'
          StatusCode: 404
        PassthroughBehavior: WHEN_NO_MATCH
        RequestParameters:
          integration.request.path.proxy: method.request.path.proxy
        Type: AWS
        Uri:
          Fn::Sub: arn:aws:apigateway:${AWS::Region}:s3:path/${MyWebsite}/{proxy}
      MethodResponses:
      - ResponseParameters:
          method.response.header.Content-Type: true
        StatusCode: 200
      - StatusCode: 404
      RequestParameters:
        method.request.path.proxy: true
      ResourceId:
        Ref: MyWebsiteResource
      RestApiId:
        Ref: MyWebsiteRestApi
    Type: AWS::ApiGateway::Method
  MyWebsiteResource:
    Properties:
      ParentId:
        Fn::GetAtt:
        - MyWebsiteRestApi
        - RootResourceId
      PathPart: '{proxy+}'
      RestApiId:
        Ref: MyWebsiteRestApi
    Type: AWS::ApiGateway::Resource
  MyWebsiteRestApi:
    Properties:
      BinaryMediaTypes:
      - audio/mpeg
      - image/jpeg
      - image/x-png
      Name:
        Fn::Sub: my-website-rest-api-${AWS::StackName}
    Type: AWS::ApiGateway::RestApi
  MyWebsiteRole:
    Properties:
      AssumeRolePolicyDocument:
        Statement:
        - Action: sts:AssumeRole
          Effect: Allow
          Principal:
            Service: apigateway.amazonaws.com
        Version: '2012-10-17'
      Policies:
      - PolicyDocument:
          Statement:
          - Action:
            - s3:GetObject
            Effect: Allow
            Resource:
              Fn::Sub: arn:aws:s3:::${MyWebsite}/*
          Version: '2012-10-17'
        PolicyName:
          Fn::Sub: my-website-role-policy-${AWS::StackName}
    Type: AWS::IAM::Role
  MyWebsiteStage:
    Properties:
      DeploymentId:
        Ref: MyWebsiteDeployment
      RestApiId:
        Ref: MyWebsiteRestApi
      StageName: prod
    Type: AWS::ApiGateway::Stage

Solution

  • The answer is to replace the entries in BinaryMediaTypes with a single entry */*

    I don't know why this works but it does

    The answer is apparently in here -

    https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings.html

    but it doesn't make any sense to me

    Anyhow; closed