AWS apigateway 会更改 http 主体吗?我怎样才能阻止它这样做?

Does AWS apigateway change http body? How can I stop it from doing this?

AWS apigateway 会改变 http body 吗?我怎样才能阻止它这样做?

我的申请:

(1) 前端 "UI" 使用 "POST method" 发送 "http request",其中包含 "body" 到 "form-data" 中的 "zip file" .

(2) AWS "apigateway" 收到此请求并将其转发给 "Lambda Proxy"

(3) AWS "Lambda" 通过 python 编码实现接收此请求并将此 zip 文件解压缩到临时文件夹。

我面临的问题: (1) 和 (2) 工作正常,但在 (3) 中,lambda 的 pythong 程序无法解压缩文件。

我的发现:

从 "UI" 发送时,正文似乎包含 zip 文件的二进制数据 如下所示:

"PK\x03\x04\n\x00\x00\x00\x00\x00\xd6;TO\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00x2.txtPK\x03\x04\n\x00\x00\x00\x00\x00\xd6;TO\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00x1.txtPK\x01\x02\x14\x00\n\x00\x00\x00\x00\x00\xd6;TO\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00x2.txtPK\x01\x02\x14\x00\n\x00\x00\x00\x00\x00\xd6;TO\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00$\x00\x00\x00x1.txtPK\x05\x06\x00\x00\x00\x00\x02\x00\x02\x00h\x00\x00\x00H\x00\x00\x00\x00\x00"

但是在 (3) lambda 的 python 代码中,如果我们只是简单地 returns 响应如下:

response = {
    "statusCode": 200,
    "headers": {
        "lambda-response": str(event["body"])
    },
    "body": "",
    "isBase64Encoded": False
}

return 回复

会发现body中的二进制数据, 好像 apigateway 改变了内容

来自:

"PK\x03\x04\n\x00\x00\x00\x00\x00\xd6;TO\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00x2.txtPK\x03\x04\n\x00\x00\x00\x00\x00\xd6;TO\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00x1.txtPK\x01\x02\x14\x00\n\x00\x00\x00\x00\x00\xd6;TO\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00\x00\x00\x00\x00x2.txtPK\x01\x02\x14\x00\n\x00\x00\x00\x00\x00\xd6;TO\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x00\x00$\x00\x00\x00x1.txtPK\x05\x06\x00\x00\x00\x00\x02\x00\x02\x00h\x00\x00\x00H\x00\x00\x00\x00\x00"

进入:

"PK\u0003\u0004\n\u0000\u0000\u0000\u0000\u0000\ufffd;TO\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0006\u0000\u0000\u0000x2.txtPK\u0003\u0004\n\u0000\u0000\u0000\u0000\u0000\ufffd;TO\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0006\u0000\u0000\u0000x1.txtPK\u0001\u0002\u0014\u0000\n\u0000\u0000\u0000\u0000\u0000\ufffd;TO\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0006\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 \u0000\u0000\u0000\u0000\u0000\u0000\u0000x2.txtPK\u0001\u0002\u0014\u0000\n\u0000\u0000\u0000\u0000\u0000\ufffd;TO\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0006\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 \u0000\u0000\u0000$\u0000\u0000\u0000x1.txtPK\u0005\u0006\u0000\u0000\u0000\u0000\u0002\u0000\u0002\u0000h\u0000\u0000\u0000H\u0000\u0000\u0000\u0000\u0000\r\n"

这很奇怪,我能做些什么来阻止它?

(2019/12/17 更新) 下面是我正在使用的 lambda 代码。

import json # to decode json
import os   # file IO
import shutil   # file IO (use this to recursively force remove a directory)

print('Loading function')

def decompress_zip_file(src_file_path, dest_dir_path):
    '''
        Decompress a zip file into a directory.

    Args:
        src_file_path (Srting): source zip file's path.
        dest_dir_path (Srting): the destination of the output directory.
    Returns:
        isSuccess (bool): the operation is successful or not.
    '''
    error_msg = "Nothing."
    try:
        if(os.path.isdir(dest_dir_path)):
            shutil.rmtree(dest_dir_path)

        with zipfile.ZipFile(src_file_path, 'r') as zip_ref:
            zip_ref.extractall(dest_dir_path)
    except Exception as ep:
        error_msg = "Error in decompress_zip_file(), ep={}:{}".format(type(ep).__name__, str(ep))
        print(error_msg)
        return (False, error_msg)

    return (True, error_msg)


def decompress_zip_file_from_content_in_binary(src_file_in_binary, dest_dir_path):
    '''
        Decompress a zip file content into a directory.

    Args:
        src_file_in_binary (byte array): source zip file's content in binary format.
        dest_dir_path (Srting): the destination of the output directory.
    Returns:
        isSuccess (bool): the operation is successful or not.
    '''

    # write the obtained binary data into a tmp zip file
    tmp_file_path = "/tmp/tmp.zip"
    if(os.path.isfile(tmp_file_path)):
        os.remove(tmp_file_path)

    output_file = open(tmp_file_path, 'wb')
    output_file.write(src_file_in_binary)
    output_file.close()

    (isSuccess, error_msg) = decompress_zip_file(tmp_file_path, dest_dir_path)

    return (isSuccess, error_msg)

def convert_from_http_body_encoding_to_local_binary(extracted_file_from_http_body_str):
    '''
        Extract the file (in binary string format) from event['body'] encoding to local binary encoding.

    Args:
        extracted_file_from_http_body_str (string): the event['body'] file (in binary string format),.
    Returns:
        zipfile_binary1 (binary array): the conversion result.
    '''
    zipfile_binary1 = bytes(extracted_file_from_http_body_str, encoding = "ascii") # convert into a zipfile in binary format    

    return zipfile_binary1

def extract_zipfile_binary_from_body(body_str):
    '''
        Extract the zipfile (in binary format) from event['body'] string.

    Args:
        body_str (string): the event['body'] string.
    Returns:
        (binary array): the conversion result.
    '''

    retValue = ""

    tmpArray = body_str.split("application/zip") # split the content based on MIME part field data; cut the head
    if(len(tmpArray) > 1):
        retValue += "entered-Lv1."
        tmpArray = tmpArray[1].split("PK") # split the content based on zip file header.
        if(len(tmpArray) > 1):
            retValue += "entered-Lv2."
            zipfile_str = "PK" + 'PK'.join(tmpArray[1:]) # add back the zip file header            
            tmpArray = zipfile_str.split("------WebKitFormBoundary") # split the content based on MIME part field data; cut the tail
            if(len(tmpArray) > 1):                
                zipfile_str = tmpArray[0]                            
                zipfile_binary = convert_from_http_body_encoding_to_local_binary(zipfile_str)
                retValue = zipfile_binary

    return retValue

def handler(event, context):
    '''Provide an event that contains the following keys:
      - operation: one of the operations in the operations dict below
      - payload: a parameter to pass to the operation being performed
    '''    

    # set the mapping table for "operation" x "return value"
    operations = {        
        'unzip': lambda x: decompress_zip_file_from_content_in_binary(**x), # unzip an uploaded file
        'ping': lambda x: 'pong' # respond to ping req.
    }

    # because we use "Lambda Proxe", means we have api-gateway forward the whole packet without resolving it for lambda.
    event_headers = event["headers"] 
    operation = event_headers['operation']    
    event_body = event["body"] 

    if(operation == 'unzip'):
        src_file_in_binary = extract_zipfile_binary_from_body(event_body)
        payload_json = {}
        payload_json['src_file_in_binary'] = src_file_in_binary
        payload_json['dest_dir_path'] = "/tmp/tmp_zipfile_output"
        event_headers["payload"] = payload_json        

    if operation in operations:
        responseBody = operations[operation](event_headers.get('payload'))

        response = {
            "statusCode": 200,
            "headers": {
                "lambda-response": str(responseBody) # the api-gateway will forward the header to the front end.
            },
            "body": "",
            "isBase64Encoded": False
        }

        return response

    else:
        raise ValueError('Unrecognized operation "{}"'.format(operation))

以下是来自 AWS 支持的回复。 LGTM。把它留在这里,以便将来人们可以看到这个问题的解决方案。

=====================下面是 AWS 支持的回复 ================= =

嗨,

感谢您联系 AWS 高级支持。我是 Jyoti,今天我将协助您处理此案。

从案例对应中了解到您担心API网关修改 代理到您的 Lambda 函数之前的二进制数据负载。如果我的理解有误,请纠正我。

预期行为:

API 网关确实将二进制数据负载修改为 UTF-8 编码的 JSON 字符串,如果 API 配置为默认设置。因此,这是预期的行为。
请注意,根据 [1],我们必须配置 API 以支持二进制负载 我们在 API 网关中的 API。 API 网关无法按原样发送二进制文件,因为它必须发送 a JSON body 到 lambda 代理。因此,它默认将 data/payload 编码为 UTF-8。

解决方案:

为了克服上述挑战,我们需要添加所需的 二进制媒体类型(application/zip 在这种情况下)到 binaryMediaTypes 列表 在 RestApi 资源的设置页面上。有关如何实现的更多信息 这个,请参考这里 --> [2]。如果未定义此 属性,则有效载荷 如 [1] 中所述,作为 UTF-8 编码的 JSON 字符串处理。

这就是为什么您请求中的文件看起来是 UTF-8 编码的原因。配置 API 后, Lambda 接收到的事件将是一个 Base64 编码的字符串。

如果要对这个object(编码请求body或'event["body"]')进行操作, 然后您可以通过以下方式将 base64 编码的字符串解码为其原始二进制形式 以下几行(在 python 运行时):

import base64
coded_string = str(event["body"])
base64.b64decode(coded_string)

疑难解答:

我试图在我的环境中复制您的设置。而不是应用程序的前端'UI', 我使用 Postman 作为客户端,而其余设置(API 网关和 Lambda)是相似的。 我正在从 Postman 向我的 API 发出 POST 请求,请求 headers 'Content-Type' 和 'Accept', 两者都设置为值 'application/zip',这是正在发送的二进制媒体类型,并且 也在响应中预期。我的 API 已配置为支持二进制媒体类型 传入请求 body。我在 API 的 binaryMediaTypes 列表中添加了 'application/zip'。 最后,在 Lambda 函数中,我正在解码 base64 编码的请求 body(即事件 ["body"]) 使用 base64 库(在 python 中)将其转换为原始二进制形式。

如果您仍想通过 returning 二进制文件确认请求的 form-data 的一致性 响应中的数据,您可以参考以下代码段:

response {
        'isBase64Encoded': True,                            #Ensure the body is base encoded 
        'statusCode': 200,
        'headers': { "Content-Type": "applicaiton/zip" },   #Define the Content-Type
        'body': event["body"]                               #Response Body returns the Base64-encoded value    
    }

我们将 isBase64Encoded 参数设置为 True,API 网关自动解码 响应 body 取决于 Content-Type (即二进制 data/media 类型) 客户端(在我的例子中是 Postman)设置为接收(即 application/zip)。请注意,'Accept' 我在 header 中发送的 header 是为了验证响应 body 是否包含二进制文件 数据类型,提出请求。

上面的响应body与第一次发送的请求body二进制数据相同 通过 API,在我的设置中。

希望我已经解决了您的顾虑。但是,如果您仍然需要实施方面的帮助, 请再次与我们联系,我很乐意为您提供帮助。

References: 
=-=-=-=--=-=-=-=-=-=
[1] Support Binary Payloads in API Gateway: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings.html 
[2] Enable Binary Support Using the API Gateway Console: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings-configure-with-console.html 

此致,

乔蒂·普拉卡什 P. 亚马逊网络服务

2019/12/20更新

我意识到我的内容类型实际上是 multipart 而不是 application/zip 所以我再次修改了设置然后它起作用了。 以下是来自 AWS 支持的帮助。非常感谢他们的帮助。

嗨,

非常感谢您详细说明您的申请流程和日志。我现在了解到您的 HTTP 请求 header 'content-type' 设置为 'multipart/form-data'。我同意,对于上传文件的网络表单,将内容类型设置为 form-data 是很常见的,而 AWS API 网关确实支持它。您想知道是否可以在不更改前端代码的情况下阻止 UTF-8 编码。如果我的理解有误,请纠正我。

为了讨论方便,我想把HTTP请求和响应的故障排除方法分开。

对于 API 的请求:

请在 API 网关控制台的“API 设置页面的 binaryMediaType 列表中添加 'multipart/form-data' 作为值之一。您不必更改代码或HTTP 请求或其任何 headers。请注意在 API 网关中处理二进制 media/data,HTTP 请求 Content-Type header 必须匹配 binaryMediaType 中的值列表。

在你的用例中,如果你想发送二进制媒体返回对您的请求的响应,HTTP 请求 'Content-Type' 和 'Accept' headers,API 的 binaryMediaType 值和 HTTP 响应 'Content-Type'必须全部设置为 'multipart/form-data'。我尝试了上面的方法,它对 Postman Client 很有效。如果 HTTP 请求 'Content-Type' 设置为 'multipart/form-data',则 'boundary' 指令由 Postman 自动设置。因此,您只需在 'binaryMediaType' 列表中添加 'multipart/form-data'。请查看我的 HTTP 请求,如下所示:


POST /stg-with-logs HTTP/1.1
Host: <some-api-id>.execute-api.us-east-1.amazonaws.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Accept: multipart/form-data
Cache-Control: no-cache
Postman-Token: 123b64f9-5669-f794-b9df-34a7561e9708

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="File"; filename="archive.zip"
Content-Type: application/zip


------WebKitFormBoundary7MA4YWxkTrZu0gW--

对于来自 API 的响应:

我在查看您的 API 网关日志时注意到,header 'isBase64Encoded' 未设置。请将其设置为 true。 API 如果 'isBase64Encoded' 设置为 true,网关会自动解码 HTTP 响应的 body 中的任何 base64 编码字符串。请查看下面来自我的 lambda 的 HTTP 响应:


(a6729f56-b245-45a4-9ac4-7e00b23c8957) Endpoint response body before transformations: 
{
    "isBase64Encoded": true,
    "statusCode": 200,
    "headers": {
        "Content-Type": "multipart/form-data",
        "Accpet": "multipart/form-data"
    },
    "body": "LS0tLS0tV2ViS2l0Rm9ybUJvdW5kYXJ5SmxkSW1aV1lHczlSTndPWQ0KQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPSJGaWxlIjsgZmlsZW5hbWU9ImFyY2hpdmUuemlwIg0KQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi96aXANCg0KUEsDBBQAAAAIAKF4kE9/Mo7/XgAAAJcAAAAaABwASGVsbG8tV29ybGQtNjY3MzMxNTI4MS50eHRVVAkAA8ZP910SUPdddXgLAAEEHZHreQTMewNxNY1BDgIxDAPvvIVPOY3SEC+9WCrfJ13EZWTNHKwKkzMmxIp5dpsnFMlqrjzBF/SKxCW2/8dl3ttGGjTqnkdMG+Wwj96jA3/YJsC2QF9iesuLUXPfv80KrpaVYeDjC1BLAQIeAxQAAAAIAKF4kE9/Mo7/XgAAAJcAAAAaABgAAAAAAAEAAACkgQAAAABIZWxsby1Xb3JsZC02NjczMzE1MjgxLnR4dFVUBQADxk/3XXV4CwABBB2R63kEzHsDcVBLBQYAAAAAAQABAGAAAACyAAAAAAANCi0tLS0tLVdlYktpdEZvcm1Cb3VuZGFyeUpsZEltWldZR3M5Uk53T1kNCkNvbnRlbnQtRGlzcG9zaXRpb246IGZvcm0tZGF0YTsgbmFtZT0iVGVzdCBEYXRhIg0KDQpUZXN0aW5nIEJvdW5kYXJ5IGluIG11bHRpcGFydC9mb3JtLWRhdGENCi0tLS0tLVdlYktpdEZvcm1Cb3VuZGFyeUpsZEltWldZR3M5Uk53T1ktLQ0K"
}

我附上了我的 API Gateway Swagger 文件和 Lambda 函数代码,以供参考。该设置对我来说工作正常,我能够在发出 HTTP 请求时 return 二进制负载。如果您想在您的环境中对其进行测试,请在 Swagger 文件中设置适当的凭据和 lambda uri。

希望这能解决您的问题。但是,如果问题仍然存在或您有任何其他问题,请再次与我们联系,我们很乐意为您提供帮助。

要查看此信件中包含的名为 'binaryPost-stg-with-logs-oas30-apigateway.yaml,python-binary-response.py' 的文件,请使用签名下方给出的大小写 link。

此致,

乔蒂·普拉卡什 P. 亚马逊网络服务

查看 AWS Support 知识中心,这是一个包含文章和视频的知识库,可以回答客户有关 AWS 服务的问题:https://aws.amazon.com/premiumsupport/knowledge-center/?icmpid=support_email_category