การตั้งค่า HTTP Response สำหรับ API Gateway ในการทำงานร่วมกับ AWS Lambda
Tags: awsthai, lambda, apigateway, aws
Published: 2021-09-24 10:33:11

__หมายเหตุ: บทความนี้กล่าวถึง Lambda custom integration เท่านั้น โดยผลลัพธ์เมื่อเลือกเป็น Lambda proxy integration จะได้ผลลัพธ์อีกแบบ__

หลายๆคนน่าจะเคยสร้าง AWS Lambda Function ขึ้นมาแล้วสร้าง REST API บน API Gateway มาเชื่อมต่อกับ Function ที่สร้างขึ้น ซึ่งขั้นตอนการสร้างดังกล่าวค่อนข้างง่ายและรวดเร็ว แต่ว่าในขั้นตอนง่ายๆนี้หลายคนอาจจะมองข้ามการตั้งค่าที่สำคัญที่อาจจะทำให้การทำงานผิดพลาดได้ โดยเฉพาะถ้า Developer คนนั้นคุ้นเคยกับการเขียนโปรแกรมบนเครื่องส่วนตัว เช่นการเขียนบน Framework หรือการใช้ Express.js ในการจัดการ Web server.

เพื่อให้เห็นภาพของความผิดพลาดนี้มากขึ้น เราจะเริ่มทดลองจากการสร้าง Function ง่ายๆขึ้นมาและทำการทดสอบเรียกด้วย Postman 

```javascript
exports.handler = async (event) => {
    // TODO implement
    const response = {
        statusCode: 400,  // Change from 200 to 400
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};
```
ตัวอย่างแรก ใน Template Nodejs ที่ AWS ให้มาเมื่อสร้าง Function แบบ "Author from scratch" บน AWS Lambda แล้วลองแก้ค่า statusCode จาก 200 เป็น 400 ซึ่งถ้าดูผ่านๆจะเข้าใจว่าเมื่อเรียก Function นี้จะได้ค่า HTTP 400 มาแทน 200 ที่ตั้งไว้เดิม แต่ว่าเมื่อลองยิงแล้วเราจะได้ HTTP 200 ตามเดิมมา แต่ในเนื้อหาจะตั้งค่า `statusCode: 400` ไว้ ซึ่งตรงนี้เข้าใจได้ง่ายๆว่าเพราะเรา return content ไม่ใช่ตั้งค่า statuscode โดยตรง.

(ในกรณี Lambda proxy integration จะได้ ผลลัพธ์ HTTP 400 Bad Request ถูกต้อง)

_( ถ้าไม่คุ้นเคยกับ HTTP Status [อ่านที่นี่](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) )_

![HTTP 200 with statusCode 400](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e3olqmdqyjjf3njx7c7a.png)

เพื่อให้เห็นภาพชัดเจนขึ้น ลองเปลี่ยน Code ใหม่ให้มีการ Throw Error อยู่ใน Code ดูบ้าง

```javascript
exports.handler = async (event) => {
    // TODO implement
    throw new Error('Internal Error - Intentionally throw this error');
};
```
เมื่อลองเรียกดูแล้วจะพบว่าผลลัพธ์ที่ได้ คือ HTTP 200 (OK) ที่มาพร้อมกับ Error message
(ในกรณี Lambda proxy integration จะได้ ผลลัพธ์ HTTP 502 Bad Gateway ถูกต้อง)

![HTTP 200 with Error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t1b78al18zltk0tosksb.png)

> กรณีแบบนี้เหมือนกับว่า Lambda ไม่ Ok นะแต่ API Gateway บอกว่ามัน Ok 
![It's Okay to not be Okay](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ylit5lchgulqxj0wgmy2.jpg)
_(ภาพจาก [Beartai.com](https://www.beartai.com/lifestyle/449363) Seo Ye-ji สวยมว๊ากกกก ซีรีย์ดีแนะนำครับ)_



ถ้าใครเคยพัฒนา NodeJS บน ExpressJS หรือใช้ Web Framework อื่นๆ น่าจะคุ้นเคยดี ว่ากับการ throw Error ใน Code แล้วได้ HTTP 500 โดยอัตโนมัติ ซึ่เหตุผลที่ได้แบบนั้นเพราะว่า Framework ทำการ Handle Error ต่างๆให้แล้วสร้าง HTTP Response ที่เหมาะสม เนื่องจากเราทำงานอยู่ใต้ Framework นั้นๆ แต่สำหรับ AWS Lambda กับ API Gateway ซึ่งเป็นคนละระบบกันโดยสิ้นเชิง แต่รองรับการเชื่อมต่อ (Integrate) ระหว่างกัน ดังนั้นสิ่งที่ตอบกลับมา HTTP200 ซึ่งเป็นค่าปกติของ API Gateway นั้นจึงมีความหมายประมาณว่า __Code ของคุณถูกเรียกใช้แล้ว__ 


## ตั้งค่า Method Response บน API Gateway ##

อันดับแรกจะต้องกำหนด HTTP Status ต่างๆที่เป็นไปได้ที่ API Gateway จะตอบกลับไปยังผู้เรียก โดยการตั้งค่าทำได้ที่หัวข้อ Method Response บน API Gateway โดยในกรณีตัวอย่างจะมีการตอบ Error 2 แบบคือ
* HTTP 500 (Internal Server Error) สำหรับกรณีที่มีการสร้าง Error ขึ้นด้วยการเรียกคำสั่ง `throw Error()` (ตัวอย่างกรณีที่ 2) โดยในเอกสารของ AWS จะเรียก Error ประเภทนี้ว่า **Standard Lambda Error**
* HTTP 400 (Bad Request) สำหรับกรณีที่มีการกำหนด Response แบบมีโครงสร้างข้อมูล Error ที่เรากำหนดไว้ โดยอาจจะมีการสร้างเป็น Class หรือ Object ที่มีรูปแบบตายตัวและทำการกำหนดตัวเลข Error Code ไว้ใน Object ดังกล่าว (ตัวอย่างกรณีที่ 1) โดยในเอกสารของ AWS จะเรียก Error ประเภทนี้ว่า **Custom Lambda Error**
![Method Response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/um91vldcu7g1kxpw0izx.png)

## ตั้งค่า Integration Response บน API Gateway ##
เมื่อเรากำหนด HTTP Status ที่จะทำการตอบกลับแล้วในขั้นตอนถัดไปจะเป็นการกำหนดให้ API Gateway อ่านค่าที่ตอบกลับมาจาก AWS Lambda และแปลงข้อมูลเป็น Status ตามที่กำหนด โดยในการอ่านค่า API Gateway จะใช้ Regular Expression ในการทดสอบข้อความในส่วน errorMessage ที่ตอบกลับมา ซึ่งถ้า Regular Expression Pattern ที่กำหนดตรงกับข้อมูลที่ได้รับ API Gateway จะเลือกตอบกลับด้วย HTTP Status ตาที่กำหนดคู่กับ Pattern นั้นๆ นอกจากนี้สำหรับการตังค่า Body Content ที่ API Gateway ตอบกลับมาจะสามารถตั้งค่าได้ด้วย Mapping Template เพื่อกำหนดรูปแบบข้อมูลที่ส่งกลับได้ด้วย


### กรณี Standard Lambda Error (500) ###
ในกรณี Error ที่เกิดจากการเรียก `throw Error()` ซึ่ง Lambda จะสร้าง Error Object ตอบกลับไปยัง API Gateway โดยอัตโนมัติและมี `errorMessage` เป็นส่วนประกอบตามรูปผลการทดสอบในกรณีที่ 2 ข้างต้น
โดยในตัวอย่าง เราจะทำการกำหนดข้อความให้ขึ้นต้นด้วยคำว่า 'Internal Error' ไว้หน้ารายละเอียดของ Error ที่ถูกสร้างขึ้น เพื่อให้ง่ายต่อการกำหนด regular expression ที่จะใช้ทดสอบ ซึ่งจะสามารถกำหนด Lambda Error Regexเป็น `^Internal Error.*` เพื่อให้ Match กับข้อความ Error ได้ อย่างไรก็ตามจากการทดสอบ กรณีที่เป็น partial match หรือ เป็นการที่ regular expression pattern ตรงกับ errorMessage เพียงบางส่วน จะไม่ถือว่าเป็นกรณีที่กำหนด และ API Gateway จะตอบกลับ 200 ตามค่ามาตรฐาน

![Config HTTP 500](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1epym7pvbwwwfpvbz14u.png)

เมื่อทดสอบด้วย Postman จะได้ผลลัพธ์ถูกต้องโดย ได้รับข้อมูล HTTP 500 (Internal Server Error) พร้อมกับข้อมูล Error object ต้นฉบับ

![Postman Test HTTP 500](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xiv1qvl4yw3iims260xi.png)

เนื่องจากการแสดง Stack trace ส่งไปให้ผู้เรียกใช้ อาจจะไม่ค่อยเหมาะสม เราสามารถกำหนดเนื้อหาของข้อมูลที่จะส่งกลับไปยังผู้เรียกใช้ได้ โดยการตั้งค่า Mapping Template และกำหนดชนิดของข้อมูลเป็น `application/json` และตั้งค่าให้ตอบกลับเฉพาะ errorMessage เท่านั้น
![Config Mapping for HTTP 500](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qd3noygm1be0vf2ydkkb.png)
(อย่าลืมกดปุ่ม __Save__ สีเทาด้านล่าง เพื่อบันทึก Template แทนการกด __Save__ สีน้ำเงินด้านบน ที่เป็นการบันทึกเมื่อแก้ Lambda Error Regex - ส่วนตัวคิดว่าการออกแบบ UX ตรงนี้แปลกๆไปหน่อย ทำให้สับสนได้ง่ายมาก และกดผิดไปหลายครั้งมาก)

เมื่อทำการ Deploy และทดสอบ จะได้ข้อมูลตอบกลับที่ถูกต้องตามที่ต้องการ โดยได้ สถานะ HTTP500 และแสดงเฉพาะข้อความ errorMessage ที่กำหนด
![Correct HTTP 500 Response](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6r6e9ejuxjut0jzq1oag.png)


### กรณี Custom Lambda Error (400) ###
ในกรณีที่มีการกำหนดโครงสร้างข้อมูลตอบกลับไว้ โดยอาจจะเป็นการกำหนด Class หรือ Response Object ตามที่ต้องการ โดยการตอบ Error Response จะใช้โครงสร้างข้อมูลแบบเดียวกับ Success Response ตามตัวอย่างกรณีที่ 1
วิธีการที่ง่ายที่สุดที่จะทำให้ API Gateway ทำการตรวจสอบข้อความที่ส่งออกไปและทำการเลือกส่ง HTTP Status ได้อย่างเหมาะสมคือการทำให้เกิดการ Error ด้วยคำสั่ง `context.fail()` แทนการ return ใน Code ตัวอย่างข้างต้น โดยส่งข้อมูล response ไปในรูปแบบ JSON string ไปยัง Error ที่ถูกสร้างขึ้นเมื่อเรียก `context.fail()` ซึ่งเมื่อทำการแก้ไขแล้วจะได้ Code ดังนี้
```JavaScript
exports.handler = async (event, context) => {
    // TODO implement
    const response = {
        statusCode: 400,  // Change from 200 to 400
        body: JSON.stringify('Hello from Lambda!'),
    };
    context.fail(JSON.stringify(response))
};
```
เมื่อ Function ถูกเรียก Lambda จะสร้าง Error Object แบบเดียวกับกรณี HTTP 500 ข้างต้น โดยนำข้อมูล Response Object ที่เรากำหนดไปเป็น errorMessage เพื่อให้ API Gateway ตรวจสอบ ซึ่งเราสามารถกำหนด Regular expression pattern คือ `.*"statusCode":400.*` เพื่อให้ Match กับ statusCode ที่อยู่ใน Response object ได้ และแสดง สถานะตอบกลับได้อย่างถูกต้อง
![Config HTTP400](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/js4qbkurun09oqnq7xs7.png)

เมื่อทำการทดสอบแล้วจะได้ผลลัพธ์เป็น HTTP Status 400 (Bad Request) ที่ส่งมาพร้อมกับ Lambda error object ที่ถูกสร้างขึ้น
![HTTP400 with Error Object](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k1gjr1l00p1b723gpwnh.png)

ในขั้นตอนสุดท้ายเพื่อที่จะทำให้แสดงผล Response content เป็น JSON ตามที่กำหนดไว้ จะต้องทำการสร้าง Mapping Template เพื่อนำ errorMessage กลับมาแสดงเป็น Response Content ตามเดิม
![HTTP400 Mapping Template](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bu65s4i8qiid4tw7iwsd.png)

เมื่อทดสอบอีกครั้งจะได้ Response Object ที่เรากำหนดไว้เพร้อมกับสถานะ HTTP400 ที่ถูกต้องตามที่กำหนด
![HTTP400 with response data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j20vfy7hu1dr4su4exaa.png)

## บทสรุป ##
สำหรับคนที่เพิ่งเริ่มหัดใช้ Lambda และ API Gateway อาจจะเคยเจอปัญหานี้ ซึ่งหวังว่าจะมีประโยชน์กับคนที่ผ่านเข้ามาดูบ้างไม่มากก็น้อยนะครับ ทั้งนี้โดยส่วนตัวเองจะชอบใช้งาน Lambda proxy integration มากการเพราะสามารถควบคุมการทำงานต่างจาก code ได้โดยตรงมากกว่าการใช้งานแบบ Lambda custom integration ตามตัวอย่างในบทความนี้ 

สุดท้ายนี้ถ้ามีคำถาม คำแนะนำ หรือข้อเสนอแนะใดๆ ผมยินดีเป็นอย่างยิ่งที่จะได้สนทนาแลกเปลี่ยนกับทุกๆท่านที่ผ่านเข้ามาคับ ขอให้สนุกกับการพัฒนาซอฟแวร์นะครับ

## เอกสารอ้างอิง ##
* [Handle Lambda errors in API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/handle-errors-in-lambda-integration.html#handle-standard-errors-in-lambda-integration)