コンテンツにスキップ

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

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

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

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

入力 (data)

ファンクションのエントリポイントでは、引数 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リクエストから情報を取得する

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

  • req.method: HTTPリクエストのメソッド
  • req.query: クエリパラメータ
  • req.body: リクエストボディ
  • req.get(): ヘッダーの値を取得する

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

許可されている標準ヘッダー

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

これ以外のヘッダーが必要な場合は craft-functions-custom-headers を利用できます。このヘッダーにより、ファンクション内で追加情報が扱えるようになります。

res でHTTPレスポンスを行う

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 メソッドでファンクションを実行する方法を紹介します。

GET リクエストの送信

以下は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 として参照できます。

POST リクエストの送信

以下は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タイプのファンクションのルール

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

必ず 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!');
}

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タイプのファンクション特有の仕様を説明します。

特定のエラーレスポンス

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

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

サンプルコード

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

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

以下は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のリファレンスを参照してください。

CORSへの対応

ブラウザから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!');
}