コンテンツにスキップ

HTTPタイプのファンクションを作成する

このページでは、HTTPタイプのファンクションの記法およびファンクション作成時のルールについて説明します。

HTTP タイプのファンクションの記法

Section titled “HTTP タイプのファンクションの記法”

ファンクションの実行ランタイムやエントリポイント、利用できるモジュールは ファンクションの書き方 で説明しました。ここでは主に、ファンクションの入力データについて説明します。

ファンクションのエントリポイントでは、引数 data で入力値を受け取ります。HTTPタイプのファンクションの場合、data として req オブジェクトと res オブジェクトを値に持つオブジェクトを受け取ります。

export default async function (data, {}) {
// data は req, res オブジェクトを持つ
const { req, res } = data;
// ... 後続処理を書く
}

これらの値はそれぞれ、Express.jsリクエスト(req) オブジェクトと レスポンス(res) オブジェクトです。HTTPタイプのファンクションではこれらの値を使ってHTTPリクエストを処理します。

req で HTTP リクエストから情報を取得する

Section titled “req で HTTP リクエストから情報を取得する”

req オブジェクトにはHTTPリクエストの情報が含まれています。例えば次のプロパティにアクセスできます。

  • req.method: HTTPリクエストのメソッド
  • req.query: クエリパラメータ
  • req.body: リクエストボディ
  • req.ip: ロードバランサーが接続元として認識したIPアドレス(req.ip とアクセス元 IP アドレス を参照)
  • req.get(): リクエストヘッダーの値を取得する

なお、Craft Functionsでは body-parser を使ってContent-Typeヘッダーに基づいてリクエスト本文を解析しています。そのため、 req.body および req.rawBody オブジェクトに直接アクセスできます。

許可されているリクエストヘッダー

Section titled “許可されているリクエストヘッダー”

Craft Functionsでは、次のリクエストヘッダーが許可されています。

  • host
  • accept
  • accept-encoding
  • accept-language
  • access-control-request-method
  • access-control-request-headers
  • cache-control
  • cookie
  • connection
  • forwarded
  • priority
  • sec-ch-ua
  • sec-ch-ua-mobile
  • sec-ch-ua-platform
  • sec-fetch-dest
  • sec-fetch-site
  • sec-fetch-user
  • upgrade-insecure-requests
  • via
  • x-forwarded-for
  • x-forwarded-proto
  • authorization
  • user-agent
  • origin
  • referer
  • content-length
  • content-type
  • x-hub-signature-256
  • x-line-signature
  • x-slack-signature
  • x-slack-request-timestamp
  • x-karte-signature
  • x-karte-request-timestamp
  • x-shopify-hmac-sha256
  • x-shopify-topic
  • x-shopify-shop-domain
  • craft-functions-custom-headers(許可リスト外のヘッダー情報を扱うための予約ヘッダー)

上記以外のリクエストヘッダーを追加する必要がある場合は、KARTE Craft | リクエストヘッダー 追加申請フォーム から申請してください。

HTTPタイプのファンクションでは res.send(), res.json() をはじめとする res オブジェクトのレスポンス用のメソッドを使ってHTTPレスポンスを行います。

以下は res.send() メソッドでプレーンテキストを返却する例です。

export default async function (data, {}) {
const { req, res } = data;
// `plain/text` を返却する
res.send("Hello!");
}

JSON形式 ( Content-Type: application/json ) で値を返す場合は res.json() メソッドを使います。

export default async function (data, {}) {
const { req, res } = data;
res.json({ name: "Karte Taro" });
}

200以外のステータスコードを返却する場合は res.status() メソッドを使用します。

export default async function (data, {}) {
const { req, res } = data;
res.status(404).send("Not Found");
}

HTTPタイプのファンクションは、ファンクションのエンドポイントURLにHTTPリクエストを行うことで実行できます。エンドポイントURLはファンクション詳細画面の「設定」タブで確認できます。

  • ファンクションのURLは https://DOMAIN_PREFIX.cev2.karte.io/functions/ENDPOINT_SUFFIX の形式です。
    • DOMAIN_PREFIX はKARTEプロジェクトごとに付与されます。
    • ENDPOINT_SUFFIX はファンクションごとに付与されます。

HTTPタイプのファンクションは次のメソッドに対応しています。

  • GET
  • POST
  • PUT
  • DELETE
  • OPTIONS

ここでは、curlコマンドで GET および POST メソッドでファンクションを実行する方法を紹介します。

以下はcurlコマンドでファンクションのエンドポイントURLに name, age クエリパラメータと共に GET リクエストを送信する例です。

curl 'https://DOMAIN_PREFIX.cev2.karte.io/functions/ENDPOINT_SUFFIX?name=Taro&age=20'
  • DOMAIN_PREFIX および ENDPOINT_SUFFIX はそれぞれ、実際のエンドポイントURLのものに置き換えます。
  • クエリパラメータ ( ?name=Taro&age=20 ) は、ファンクション内で req.query として参照できます。

以下はcurlコマンドでファンクションのエンドポイントURLに POST リクエストを送信する例です。

curl 'https://DOMAIN_PREFIX.cev2.karte.io/functions/ENDPOINT_SUFFIX' \
-H 'Content-Type: application/json' \
-d '{
"name": "Taro",
"age": 20
}'
  • DOMAIN_PREFIX および ENDPOINT_SUFFIX はそれぞれ、実際のエンドポイントURLのものに置き換えます。
  • リクエストボディ ( -d オプションの値) は、ファンクション内で req.body として参照できます。

HTTP タイプのファンクションのルール

Section titled “HTTP タイプのファンクションのルール”

HTTPタイプのファンクション特有のルールを説明します。

必ず res で明示的にレスポンスする

Section titled “必ず res で明示的にレスポンスする”

HTTPタイプのファンクションでは、res オブジェクトのメソッドを使ってHTTPレスポンスを送信してください。

次の例では res オブジェクトでレスポンスを行わずに return で値を返却しようとしています。HTTPタイプのファンクションは、return で返り値を渡してもHTTPレスポンスを行いません。

NGな例
export default async function (data, {}) {
const { req, res } = data;
// NG: res でレスポンスせずに return する
return "Hello!";
}

このような場合は、例えば次のように、res.send() メソッドでHTTPレスポンスを送ります。

OKな例
export default async function (data, {}) {
const { req, res } = data;
// NG: res でレスポンスせずに return する
return 'Hello!';
// OK: res オブジェクトのメソッドを使ってHTTPレスポンスを行う
res.send('Hello!');
}

ファンクション内でHTTPレスポンスを行わない場合、Craft Functionsは自動的に 599 ステータスのエラーレスポンスを返却します。

res によるレスポンスは1回の実行につき 1 回のみ

Section titled “res によるレスポンスは1回の実行につき 1 回のみ”

res.send(), res.json() 等のHTTPレスポンスを行うメソッドは、ファンクションの1実行につき1回のみ使えます。2回目以降はエラーになるので注意しましょう。

例えば、次のコードはリクエストのContent-Typeによって異なるレスポンスを行います。このコードは、Content-Type: application/json のリクエストを受け取った際にレスポンス用のメソッドを2回呼び出します。

エラーになる例
export default async function (data, {}) {
const { req, res } = data;
const contentType = req.get("content-type");
if (contentType === "application/json") {
({ name } = req.body);
res.json({ message: `Hello ${name}` });
}
// application/json タイプのリクエストを受け取ると以下のエラーになる
// Error: Can't set headers after they are sent. ...
res.send({ message: `Hello World!` });
}

改善方法として、if文内でHTTPレスポンスを行う際に return して、後続処理をスキップする方法があります。

改善例
if (contentType === 'application/json') {
({ name } = req.body);
res.json({ message: `Hello ${name}` });
return res.json({ message: `Hello ${name}` });
}

ファンクションの記法に関わらない、HTTPタイプのファンクション特有の仕様を説明します。

req.ip には、ロードバランサーが接続元として認識したIPアドレスが入ります。

CDNやリバースプロキシを経由するリクエストでは、エンドユーザーのIPではなくそちらの送信元IPになる場合があります。

接続元IPアドレスによるリクエストのフィルタリングを自動的に行う機能は提供していません。接続元IPによる制限は、ファンクションのコード内で req.ip を参照して実装してください。

ファンクションの終了ステータスは res.status() で柔軟に設定できます。ただし、次の状況ではファンクションは特定のステータスコードを返却します。

  • res によるレスポンスを行わずにファンクションを終了した場合、ファンクションはステータスコード599を返却します。
  • タイムアウトした場合、ファンクションはステータスコード504を返却します。

HTTPタイプのファンクションのサンプルコードを紹介します。

リクエストの内容に基づきレスポンスを変える

Section titled “リクエストの内容に基づきレスポンスを変える”

以下はHTTPタイプのファンクションのサンプルコードです。このコードでは次の処理を行っています。

  • HTTPリクエストのメソッドを検証
    • POST リクエストのみを許容する
  • リクエストの Content-Type ヘッダーの値によってHTTPレスポンスの内容を変える
    • application/json の場合はリクエストボディの name プロパティの値を使ってJSON文字列でレスポンスを行う
    • text/plain の場合はリクエストボディの値をそのまま使ってテキストメッセージをレスポンスを行う
    • それ以外の場合は固定のメッセージでレスポンスを行う
const LOG_LEVEL = "DEBUG";
export default async function (data, { MODULES }) {
const { req, res } = data;
const { initLogger } = MODULES;
const logger = initLogger({ logLevel: LOG_LEVEL });
// HTTPメソッドがPOSTでなければ処理を終了
if (req.method !== "POST") {
res.setHeader("Allow", "POST");
res.status(405).send("Method Not Allowed");
return;
}
let name;
// リクエストのContent-Typeに応じて処理を分岐
switch (req.get("content-type")) {
case "application/json":
// Content-Typeがapplication/jsonの場合の処理
logger.log(`content-type is application/json`);
({ name } = req.body);
// 可読性のために明示的に 200 を返してもよい
res.status(200).json({ message: `Hello ${name}` });
break;
case "text/plain":
// Content-Typeがtext/plainの場合の処理
logger.log(`content-type is text/plain`);
name = req.body;
res.status(200).send(`Hello ${name}`);
break;
default:
// どのContent-Typeにも該当しない場合の処理
name = "World!";
res.status(200).json({ message: `Hello ${name}` });
}
}

req, res オブジェクトのより詳しい利用方法については、Express.jsのリファレンスを参照してください。

ブラウザからURLエンドポイントにリクエストする際には CORS (オリジン管理ソース共有、Cross-Origin Resource Sharing) への対応が必要です。具体的には、クライアントからのプリフライトリクエストに応答してください。

以下は特定のオリジン ( https://hoge.example.com ) に対してクロスオリジンリクエストを許可する場合のレスポンス例です。

const ALLOWED_ORIGIN = "https://hoge.example.com";
export default async function (data, {}) {
const { req, res } = data;
// GETリクエストについても Access-Control-Allow-Origin ヘッダーを付与
res.set("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
if (req.method === "OPTIONS") {
res.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.set("Access-Control-Allow-Headers", "Content-Type");
res.set("Access-Control-Allow-Credentials", true);
return res.status(204).send("");
}
res.send("hello!");
}

複数のオリジンに許可をする場合は、次のようにファンクション側で許可対象のオリジンリストを保持して、リクエスト内容と比較した上で Access-Control-Allow-Origin の値を指定します。

const ALLOWED_ORIGINS = [
"https://hoge.example.com",
"https://fuga.example.com",
];
export default async function (data, {}) {
const { req, res } = data;
// Origin ヘッダーの値を検証して Access-Control-Allow-Origin ヘッダーに値を付与する
const srcOrigin = req.get("origin");
if (ALLOWED_ORIGINS.includes(srcOrigin)) {
res.set("Access-Control-Allow-Origin", srcOrigin);
}
if (req.method === "OPTIONS") {
res.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.set("Access-Control-Allow-Headers", "Content-Type");
res.set("Access-Control-Allow-Credentials", true);
return res.status(204).send("");
}
res.send("hello!");
}

イベント駆動タイプのファンクションの エンドポイント トリガーと同等のレスポンスをする場合は次のようにOriginヘッダーの値をそのまま Access-Control-Allow-Origin ヘッダーの値に利用します。

export default async function (data, {}) {
const { req, res } = data;
// Origin ヘッダーの値を検証して Access-Control-Allow-Origin ヘッダーに値を付与する
const srcOrigin = req.get("origin");
if (srcOrigin) res.set("Access-Control-Allow-Origin", srcOrigin);
if (req.method === "OPTIONS") {
res.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.set("Access-Control-Allow-Headers", "Content-Type");
res.set("Access-Control-Allow-Credentials", true);
return res.status(204).send("");
}
res.send("hello!");
}