简体   繁体   English

使用 Lambda 修改 Cloudfront 原始响应 - 只读标头

[英]Modify Cloudfront origin response with Lambda - read-only headers

I have a Cloudfront distribution with a single React site, which is hosting in S3.我有一个带有单个 React 站点的 Cloudfront 发行版,该站点托管在 S3 中。 The origin is connected via REST api.原点通过 REST api 连接。 To properly handle queries, I use custom error responses on status 403 and 404 to 200 and route them to root.为了正确处理查询,我对状态 403 和 404 到 200 使用自定义错误响应,并将它们路由到 root。 The root object is index.html and everything seems to be fine.根 object 是 index.html ,一切似乎都很好。

Now I have a task to add to a distribution an another site, which should be accessible through a subdirectory.现在我的任务是向分发中添加另一个站点,该站点应该可以通过子目录访问。 To do this I have to set a root object for a subdirectory and to catch 404 and 403 responses and transfer them to a root object.为此,我必须为子目录设置根 object 并捕获 404 和 403 响应并将它们传输到根 object。 I've already set up origin and behaviour.我已经设置了起源和行为。

I tried to use theese manuals: example source but it seems that something went wrong我尝试使用这些手册: 示例,但似乎出了点问题

The first approach (CloudFrontSubdirectoryIndex) seems not working at all (the function is not invoked and no rewrite happens), so i tried CloudFront function and it seems to work fine.第一种方法(CloudFrontSubdirectoryIndex)似乎根本不起作用(没有调用 function,也没有发生重写),所以我尝试了CloudFront function ,它似乎工作正常。

The last step is to handle 404 and 403 responses.最后一步是处理 404 和 403 响应。 Here is the function from the manual:这是手册中的 function:

'use strict';

const http = require('https');

const indexPage = 'index.html';

exports.handler = async (event, context, callback) => {
    const cf = event.Records[0].cf;
    const request = cf.request;
    const response = cf.response;
    const statusCode = response.status;
    
    // Only replace 403 and 404 requests typically received
    // when loading a page for a SPA that uses client-side routing
    const doReplace = request.method === 'GET'
                    && (statusCode == '403' || statusCode == '404');
    
    const result = doReplace 
        ? await generateResponseAndLog(cf, request, indexPage)
        : response;
        
    callback(null, result);
};

async function generateResponseAndLog(cf, request, indexPage){
    
    const domain = cf.config.distributionDomainName;
    const appPath = getAppPath(request.uri);
    const indexPath = `/${appPath}/${indexPage}`;
    
    const response = await generateResponse(domain, indexPath);
    
    console.log('response: ' + JSON.stringify(response));
    
    return response;
}

async function generateResponse(domain, path){
    try {
        // Load HTML index from the CloudFront cache
        const s3Response = await httpGet({ hostname: domain, path: path });

        const headers = s3Response.headers || 
            {
                'content-type': [{ value: 'text/html;charset=UTF-8' }]
            };
            
        return {
            status: '200',
            headers: wrapAndFilterHeaders(headers),
            body: s3Response.body
        };
    } catch (error) {
        return {
            status: '500',
            headers:{
                'content-type': [{ value: 'text/plain' }]
            },
            body: 'An error occurred loading the page'
        };
    }
}

function httpGet(params) {
    return new Promise((resolve, reject) => {
        http.get(params, (resp) => {
            console.log(`Fetching ${params.hostname}${params.path}, status code : ${resp.statusCode}`);
            let result = {
                headers: resp.headers,
                body: ''
            };
            resp.on('data', (chunk) => { result.body += chunk; });
            resp.on('end', () => { resolve(result); });
        }).on('error', (err) => {
            console.log(`Couldn't fetch ${params.hostname}${params.path} : ${err.message}`);
            reject(err, null);
        });
    });
}

// Get the app path segment e.g. candidates.app, employers.client etc
function getAppPath(path){
    if(!path){
        return '';
    }
    
    if(path[0] === '/'){
        path = path.slice(1);
    }
    
    const segments = path.split('/');
    
    // will always have at least one segment (may be empty)
    return segments[0];
}

// Cloudfront requires header values to be wrapped in an array
function wrapAndFilterHeaders(headers){
    const allowedHeaders = [
        'content-type',
        'content-length',
        'last-modified',
        'date',
        'etag'
    ];
    
    const responseHeaders = {};
    
    if(!headers){
        return responseHeaders;
    }
    
    for(var propName in headers) {
        // only include allowed headers
        if(allowedHeaders.includes(propName.toLowerCase())){
            var header = headers[propName];
            
            if (Array.isArray(header)){
                // assume already 'wrapped' format
                responseHeaders[propName] = header;
            } else {
                // fix to required format
                responseHeaders[propName] = [{ value: header }];
            }    
        }
        
    }
    
    return responseHeaders;
}

When i try to implement this solution (attach the function to origin response) I get当我尝试实施此解决方案时(将 function 附加到原始响应)我得到

The Lambda function result failed validation: The function tried to add, delete, or change a read-only header. Lambda function 结果验证失败:function 尝试添加、删除或更改只读 Z09945EZ6953404DBF31。

Here is a list of restricted headers, but I'm not modifying any of them. 是一个受限制的标题列表,但我没有修改任何一个。 If I try not to attach any headers to a response at all, the message is the same.如果我尝试根本不将任何标题附加到响应中,则消息是相同的。 If I try to attach all headers, CloudFront says that i'm modifying a black-listed header.如果我尝试附加所有标题,CloudFront 会说我正在修改列入黑名单的 header。 Objects in a bucket have only one customized Cache-Control: no-cache metadata. Bucket 中的对象只有一个自定义的 Cache-Control:no-cache 元数据。

It seemed to be a fast task, but I'm stuck for two days already.这似乎是一项快速的任务,但我已经被困了两天了。 Any help will be appreciated.任何帮助将不胜感激。

UPD: I've searched the logs and found UPD:我搜索了日志并发现

ERROR Validation error: Lambda function result failed validation, the function tried to delete read-only header, headerName: Transfer-Encoding. ERROR Validation error: Lambda function result failed validation, the function tried to delete read-only header, headerName: Transfer-Encoding.

I'm a little bit confused.我有点困惑。 This header is not present in origin response, but CF is telling that I deleted it...这个 header 在原始响应中不存在,但 CF 告诉我我删除了它......

I tried to find the value of the header "Transfer-Encoding" that should come from origin (S3) but it seems that it has been disappeared.我试图找到应该来自原点(S3)的 header“传输编码”的值,但它似乎已经消失了。 And CloudFront says that this header is essential. CloudFront 说这个 header 是必不可少的。

So I've just hard-coded it and everything becomes fine.所以我只是对其进行了硬编码,一切都变得很好。

'use strict';

const http = require('https');

const indexPage = 'index.html';

exports.handler = async (event, context, callback) => {
    const cf = event.Records[0].cf;
    const request = cf.request;
    const response = cf.response;
    const statusCode = response.status;
    
    // Only replace 403 and 404 requests typically received
    // when loading a page for a SPA that uses client-side routing
    const doReplace = request.method === 'GET'
                    && (statusCode == '403' || statusCode == '404');
    
    const result = doReplace 
        ? await generateResponseAndLog(cf, request, indexPage)
        : response;
        
    callback(null, result);
};

async function generateResponseAndLog(cf, request, indexPage){
    
    const domain = cf.config.distributionDomainName;
    const appPath = getAppPath(request.uri);
    const indexPath = `/${appPath}/${indexPage}`;
    
    const response = await generateResponse(domain, indexPath);
    
    console.log('response: ' + JSON.stringify(response));
    
    return response;
}

async function generateResponse(domain, path){
    try {
        // Load HTML index from the CloudFront cache
        const s3Response = await httpGet({ hostname: domain, path: path });

        const headers = s3Response.headers || 
            {
                'content-type': [{ value: 'text/html;charset=UTF-8' }]
            };
        s3Response.headers['transfer-encoding'] = 'chunked';    
        return {
            status: '200',
            headers: wrapAndFilterHeaders(headers),
            body: s3Response.body
        };
    } catch (error) {
        return {
            status: '500',
            headers:{
                'content-type': [{ value: 'text/plain' }]
            },
            body: 'An error occurred loading the page'
        };
    }
}

function httpGet(params) {
    return new Promise((resolve, reject) => {
        http.get(params, (resp) => {
            console.log(`Fetching ${params.hostname}${params.path}, status code : ${resp.statusCode}`);
            let result = {
                headers: resp.headers,
                body: ''
            };
            resp.on('data', (chunk) => { result.body += chunk; });
            resp.on('end', () => { resolve(result); });
        }).on('error', (err) => {
            console.log(`Couldn't fetch ${params.hostname}${params.path} : ${err.message}`);
            reject(err, null);
        });
    });
}

// Get the app path segment e.g. candidates.app, employers.client etc
function getAppPath(path){
    if(!path){
        return '';
    }
    
    if(path[0] === '/'){
        path = path.slice(1);
    }
    
    const segments = path.split('/');
    
    // will always have at least one segment (may be empty)
    return segments[0];
}

// Cloudfront requires header values to be wrapped in an array
function wrapAndFilterHeaders(headers){
    const allowedHeaders = [
        'content-type',
        'content-length',
        'content-encoding',
        'transfer-encoding',
        'last-modified',
        'date',
        'etag'
    ];
    
    const responseHeaders = {};
    
    if(!headers){
        return responseHeaders;
    }
    
    for(var propName in headers) {
        // only include allowed headers
        if(allowedHeaders.includes(propName.toLowerCase())){
            var header = headers[propName];
            
            if (Array.isArray(header)){
                // assume already 'wrapped' format
                responseHeaders[propName] = header;
            } else {
                // fix to required format
                responseHeaders[propName] = [{ value: header }];
            }    
        }
        
    }
    
    return responseHeaders;
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 无服务器调用返回“无法编组响应:OSError(30, 'Read-only file system') for my Python lambda - Serverless invoke returns "Unable to marshal response: OSError(30, 'Read-only file system') for my Python lambda 如何将标头添加到 CloudFront 响应? - How to add headers to CloudFront response? AWS-Cloudfront-如何使用起源自定义标头 - AWS - Cloudfront - How To Use Origin Custom Headers CloudFront将自定义标头转发到原始值,但值为空 - CloudFront forwarding Custom Headers to Origin but with null Values 如何将主机标头转发到 CloudFront UI 中的源? - How to forward host headers to the origin in CloudFront UI? 使用Lambda,API Gateway和Cloudfront时,尽管在Lambda响应中指定了“ Access-Control-Allow-Origin”,但CORS错误 - CORS error despite 'Access-Control-Allow-Origin' specified in Lambda response when using Lambda, API Gateway and Cloudfront 从 lambda 返回 AWS CloudFront 地理位置标头 - Returning AWS CloudFront geolocation headers from a lambda CloudFront 源响应返回状态:“403” - CloudFront origin-response return status: '403' Cloudfront 的第二个来源给出错误的响应 - Cloudfront second origin give wrong response Lambda@Edge 与 Lambda 作为 Cloudfront Origin 的性能和成本比较 - Performance and Cost Comparison of Lambda@Edge vs Lambda as Cloudfront Origin
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM