doufei3561 2018-10-12 14:50
浏览 74
已采纳

Golang AWS API Gateway无效字符'e'寻找值的开始

I am trying to create an API Gateway connected to a lambda which parses an HTML template with handlebars and then returns it but I am getting this error when I run it locally and even on a test url using AWS.

{
"errorMessage": "invalid character 'e' looking for beginning of value",
"errorType": "SyntaxError"
}

This is my SAM Template

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: data-template-renderer
Parameters:
  Stage:
    Type: String
    AllowedValues:
    - dev
    - staging
    - production
    Description: environment values
Resources:
  # Defining the API Resource here means we can define the stage name rather than
  # always being forced to have staging/prod. Also means we can add a custom domain with
  # to the API gateway within this template if needed. Unfortunately there is a side effect
  # where it creates a stage named "Stage". This is a known bug and the issue can be
  # found at https://github.com/awslabs/serverless-application-model/issues/191
  DataTemplateRendererApi:
    Type: AWS::Serverless::Api
    Properties:
      Name: !Sub "${Stage}-data-template-renderer"
      StageName: !Ref Stage
      DefinitionBody:
        swagger: "2.0"
        basePath: !Sub "/${Stage}"
        info:
          title: !Sub "${Stage}-data-template-renderer"
          version: "1.0"
        consumes:
        - application/json
        produces:
        - application/json
        - text/plain
        - application/pdf
        paths:
          /render:
            post:
              x-amazon-apigateway-integration:
                uri:
                  "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RenderTemplate.Arn}/invocations"
                httpMethod: POST
                type: aws_proxy
        x-amazon-apigateway-binary-media-types:
        - "*/*"

  RenderTemplate:
    Type: AWS::Serverless::Function
    Properties:
      Environment:
        Variables:
          ENV: !Ref Stage
      Runtime: go1.x
      CodeUri: build
      Handler: render_template
      FunctionName: !Sub 'render_template-${Stage}'
      Timeout: 30
      Tracing: Active
      Events:
        RenderTemplateEndpoint:
          Type: Api
          Properties:
            RestApiId: !Ref DataTemplateRendererApi
            Path: /render
            Method: POST
      Policies:
      - !Ref S3AccessPolicy
      - CloudWatchPutMetricPolicy: {}

  S3AccessPolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: data-template-renderer-s3-policy
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
        - Effect: Allow
          Action: s3:GetObject
          Resource: !Sub "arn:aws:s3:::*"
        - Effect: Allow
          Action: s3:PutObject
          Resource: !Sub "arn:aws:s3:::*"

And below is the code that I am using for the Lambda. Please forgive the fact that it may not be the best Golang Code you have seen but I am a beginner when it comes to Golang, as I am primarily a PHP dev but the company I work for is creating a lot of Golang lambdas so I started learning it.

package main

import (
    "bytes"
    "context"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "strings"
    "time"

    "github.com/aymerick/raymond"
    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/SebastiaanKlippert/go-wkhtmltopdf"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/s3"
)

var sess *session.Session

func init() {
    // Setup AWS S3 Session (build once use every function)
    sess = session.Must(session.NewSession(&aws.Config{
        Region: aws.String("us-east-1"),
    }))
}

func main() {
    lambda.Start(handleRequest)
}

type TemplateRendererRequest struct {
    Template string `json:"template"`
    Bucket string `json:"bucket"`
    Type string `json:"type"`
    Data map[string]interface{} `json:"data"`
}

type EmailResponse struct {
    Email string `json:"email"`
}

func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    // Unmarshal json request body into a TemplateRendererRequest struct that mimics the json payload
    requestData := TemplateRendererRequest{}
    err := json.Unmarshal([]byte(request.Body), &requestData)
    if err != nil {
        return events.APIGatewayProxyResponse{
            Body: fmt.Errorf("Error: %s ", err.Error()).Error(),
            StatusCode: 400,
            Headers: map[string]string{
                "Content-Type": "text/plain",
            },
        }, err
    }

    // Get the template object from S3
    result, err := s3.New(sess).GetObject(&s3.GetObjectInput{
        Bucket: aws.String(requestData.Bucket),
        Key:    aws.String(requestData.Template),
    })
    if err != nil {
        return events.APIGatewayProxyResponse{
            Body: fmt.Errorf("Error: %s ", err.Error()).Error(),
            StatusCode: 400,
            Headers: map[string]string{
                "Content-Type": "text/plain",
            },
        }, err
    }
    defer result.Body.Close()

    // The S3 Object Body is of type io.ReadCloser
    // below we create a bytes buffer and then convert it to a string so we can use it later
    buf := new(bytes.Buffer)
    buf.ReadFrom(result.Body)
    templateString := buf.String()

    // Parse template through Handlebars library
    parsedTemplate, err := parseTemplate(templateString, requestData)
    if err != nil {
        return events.APIGatewayProxyResponse{
            Body: fmt.Errorf("Error: %s ", err.Error()).Error(),
            StatusCode: 400,
            Headers: map[string]string{
                "Content-Type": "text/plain",
            },
        }, err
    }

    switch requestData.Type {
    case "pdf":
        return handlePDFRequest(parsedTemplate, requestData)
    default:
        return handleEmailRequest(parsedTemplate)
    }
}

func handleEmailRequest(parsedTemplate string) (events.APIGatewayProxyResponse, error) {
    // Put email parsed template in a struct to return in the form of JSON
    emailResponse := EmailResponse{
        Email: parsedTemplate,
    }
    // JSON encode the emailResponse
    response, err := JSONMarshal(emailResponse)
    if err != nil {
        return events.APIGatewayProxyResponse{
            Body: fmt.Errorf("Error: %s ", err.Error()).Error(),
            StatusCode: 400,
            Headers: map[string]string{
                "Content-Type": "text/plain",
            },
        }, err
    }

    return events.APIGatewayProxyResponse{
        Body: string(response),
        StatusCode: 200,
        Headers: map[string]string{
            "Content-Type": "application/json",
        },
    }, nil
}

func parseTemplate(templateString string, request TemplateRendererRequest) (string, error) {
    result, err := raymond.Render(templateString, request.Data)

    return result, err
}

func handlePDFRequest(parsedTemplate string, requestData TemplateRendererRequest) (events.APIGatewayProxyResponse, error) {
    pdfBytes, err := GeneratePDF(parsedTemplate)
    if err != nil {
        return events.APIGatewayProxyResponse{
            Body: fmt.Errorf("Error: %s ", err.Error()).Error(),
            StatusCode: 400,
            Headers: map[string]string{
                "Content-Type": "text/plain",
            },
        }, err
    }

    keyNameParts := strings.Split(requestData.Template, "/")
    keyName := keyNameParts[len(keyNameParts)-1]
    pdfName := fmt.Sprintf("%s_%s.pdf", keyName, time.Now().UTC().Format("20060102150405"))
    _, err = s3.New(sess).PutObject(&s3.PutObjectInput{
        Bucket: aws.String(requestData.Bucket),
        Key:    aws.String(pdfName),
        Body:   bytes.NewReader(pdfBytes),
    })

    b64Pdf := base64.StdEncoding.EncodeToString(pdfBytes)
    return events.APIGatewayProxyResponse{
        Body: b64Pdf,
        StatusCode: 200,
        Headers: map[string]string{
            "Content-Type": "application/pdf",
        },
        IsBase64Encoded: true,
    }, nil
}

func GeneratePDF(templateString string) ([]byte, error) {
    pdfg, err := wkhtmltopdf.NewPDFGenerator()
    if err != nil {
        return nil, err
    }

    // Pass S3 Object body (as reader io.Reader) directly into wkhtmltopdf
    pdfg.AddPage(wkhtmltopdf.NewPageReader(strings.NewReader(templateString)))

    // Create PDF document in internal buffer
    if err := pdfg.Create(); err != nil {
        return nil, err
    }

    // Return PDF as bytes array
    return pdfg.Bytes(), nil
}

// https://stackoverflow.com/questions/28595664/how-to-stop-json-marshal-from-escaping-and
func JSONMarshal(t interface{}) ([]byte, error) {
    buffer := &bytes.Buffer{}
    encoder := json.NewEncoder(buffer)
    encoder.SetEscapeHTML(false)
    err := encoder.Encode(t)
    return buffer.Bytes(), err
}

I have tried to search high and low for a solution but couldn't find why am I getting this really weird error which is not very helpful. Thanks for taking the time and helping

  • 写回答

1条回答 默认 最新

  • dty9731 2018-10-12 16:51
    关注

    Thanks to @Anzel who pointed me in the right direction I decided to look at the request.Body and it seems that by adding the */* to the API Gateway Binary media types, now even the request comes in encoded and lo and behold the first letter was indeed an e as the message was saying.

    So I changed this:

    // Unmarshal json request body into a TemplateRendererRequest struct that mimics the json payload
        requestData := TemplateRendererRequest{}
        err := json.Unmarshal([]byte(request.Body), &requestData)
        if err != nil {
            return events.APIGatewayProxyResponse{
                Body: fmt.Errorf("Error: %s ", err.Error()).Error(),
                StatusCode: 400,
                Headers: map[string]string{
                    "Content-Type": "text/plain",
                },
            }, err
        }
    

    To this:

    // Unmarshal json request body into a TemplateRendererRequest struct that mimics the json payload
        requestData := TemplateRendererRequest{}
    
        b64String, _ := base64.StdEncoding.DecodeString(request.Body)
        rawIn := json.RawMessage(b64String)
        bodyBytes, err := rawIn.MarshalJSON()
        if err != nil {
            return events.APIGatewayProxyResponse{
                Body: fmt.Errorf("Error: %s ", err.Error()).Error(),
                StatusCode: 400,
                Headers: map[string]string{
                    "Content-Type": "text/plain",
                },
            }, err
        }
    
        jsonMarshalErr := json.Unmarshal(bodyBytes, &requestData)
        if jsonMarshalErr != nil {
            return events.APIGatewayProxyResponse{
                Body: fmt.Errorf("Error: %s ", jsonMarshalErr.Error()).Error(),
                StatusCode: 400,
                Headers: map[string]string{
                    "Content-Type": "text/plain",
                },
            }, jsonMarshalErr
        }
    

    From what I saw online you could also change this:

    rawIn := json.RawMessage(b64String)
    bodyBytes, err := rawIn.MarshalJSON()
    

    To this:

    []byte(b64String)
    

    When I now do a CURL request and output it to a file I get the PDF correctly.

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥30 帮我写一段可以读取LD2450数据并计算距离的Arduino代码
  • ¥15 C#调用python代码(python带有库)
  • ¥15 矩阵加法的规则是两个矩阵中对应位置的数的绝对值进行加和
  • ¥15 活动选择题。最多可以参加几个项目?
  • ¥15 飞机曲面部件如机翼,壁板等具体的孔位模型
  • ¥15 vs2019中数据导出问题
  • ¥20 云服务Linux系统TCP-MSS值修改?
  • ¥20 关于#单片机#的问题:项目:使用模拟iic与ov2640通讯环境:F407问题:读取的ID号总是0xff,自己调了调发现在读从机数据时,SDA线上并未有信号变化(语言-c语言)
  • ¥20 怎么在stm32门禁成品上增加查询记录功能
  • ¥15 Source insight编写代码后使用CCS5.2版本import之后,代码跳到注释行里面