Cloudflare Workers で、リクエストを処理するサンプル

# はじめに

みなさん、こんにちは。
KDDIウェブコミュニケーションズ、CPaaSエバンジェリストの高橋です。

今回、初めて Cloudflare の FaaS でもある [Workers](https://www.cloudflare.com/ja-jp/products/workers/) を使ってみたのですが、リクエストのパラメータ受け取り方法などが調べてもあまり出てこなかったので、こんな感じで使えば良いのではというサンプルを作成してみました。

なにせ、Cloudflare 自体が初心者なもので、これが正しいかはわかりませんが、何かのお役に立てばと思います。

:::note info
今回は Cloudflare のセットアップや、Wrangler の使い方などには触れていません。
:::

## シナリオ

このサンプルは、以下の３つのリクエストを想定した Workers になっています。これらを１つの Wokers で処理します。使用した Wrangler のバージョンは、`3.1.1`です。

メソッド|パス|クエリーパラメータ|ボディパラメータ|レスポンス
:--|:--|:--|:--|:--
GET|なし|なし|-|OK
GET|/hello|name, age, gender||Hello [name] ! Age: [age] Gender: [male\|female]
POST|なし|なし|{"key1":"value","key2":2, "key3": "a"}|{"url":"https://[URL]/","path": "","key1": "value","key2": 2,"key3": "a"}

### 実行結果

GET パス:なし、クエリーパラメータ:なし
![スクリーンショット 2023-07-13 7.11.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/2df2e438-b2a6-96fa-d96b-822148e336e0.png)

GET パス:/hello、クエリーパラメータ:name=hoge&age=20&gender=male
![スクリーンショット 2023-07-13 7.12.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/650f7929-4b2b-d883-7f73-2d2eef1b7056.png)

POST パス:なし、Body: JSON
![スクリーンショット 2023-07-13 7.11.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/945bf536-3ff0-adef-0484-2d58ba41f98f.png)

## コード

```typescript:worker.ts
import vs from 'value-schema';

export interface Env {
}

// クエリーパラメータのスキーマ定義
// Learn more at https://github.com/shimataro/value-schema
interface QueryParams {
	[key: string]: string;
}
const QuerySchema = {
	name: vs.string({
		ifUndefined: 'world',
	}),
	age: vs.number(),
	gender: vs.string({
		only: ['male', 'female'],
	}),
}

// Bodyパラメータのスキーマ定義
// Learn more at https://github.com/shimataro/value-schema
const RequestBodySchema = {
	key1: vs.string(),
	key2: vs.number({
		ifUndefined: 0,
	}),
	key3: vs.string({
		only: ['a', 'b', 'c'],
	}),
}

// パラメータのエラーハンドラ
let errors:string[] = [];
const errorHandler = (err: any) => {
	const key = err.keyStack.shift();
	errors.push(key);
}

export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
		console.info(`🐞 Function start...`);
		const url = new URL(request.url);
		const pathArray = url.pathname.split('/');
		const searchParams = url.searchParams;
		const queryParams: QueryParams = {}
		for (const [key, value] of searchParams) queryParams[key] = value;

		// メソッドとパスごとに処理を分岐
		switch (`${request.method}/${pathArray[1]}`) {
			case 'GET/':
				try {
					// レスポンスの返却
					return new Response(`OK`);				
				} catch (error) {
					console.error(`👺 ${error}`);
					return new Response(`${error}`);
				}

			case 'GET/hello':
				try {
					// クエリーパラメータのチェック
					errors = [];
					const data = vs.applySchemaObject(QuerySchema, queryParams, errorHandler, () => {
						// エラーが発生するたびに errorHandler が呼ばれ、すべてのエラーチェックが終わったらerrorを投げる
						throw new Error(`👺 Parameter error. ${errors.join(', ')}`);
					});
	
					// レスポンスの返却
					return new Response(`Hello ${data.name} ! Age: ${data.age} Gender: ${data.gender}`);				
				} catch (error) {
					console.error(`👺 ${error}`);
					return new Response(`${error}`);
				}

			case 'POST/':
				try {
					// Bodyパラメータのチェック
					errors = [];
					const data = vs.applySchemaObject(RequestBodySchema, await request.json(), errorHandler, () => {
						// エラーが発生するたびに errorHandler が呼ばれ、すべてのエラーチェックが終わったらerrorを投げる
						throw new Error(`👺 Parameter error. ${errors.join(', ')}`);
					});
					
					// レスポンスの返却
					const result = JSON.stringify({
						url: request.url,
						path: pathArray[1],
						key1: data.key1,
						key2: data.key2,
						key3: data.key3,
					});
					return new Response(result, {
						headers: {
							'content-type': 'application/json;charset=UTF-8',
						},
					})
				} catch (error) {
					console.error(`👺 ${error}`);
					return new Response(`${error}`);
				}

			default:
				throw new Error(`${request.method} is not supported.`);
		}
	},
};

```


## 解説

ではここからはコードの解説をしていきます。
今回の趣旨は、リクエスト時の各種パラメータの取得です。これらの情報は、一番最初に受け取る`Request`オブジェクトから取得することができます。
`Request`オブジェクトは、以下のような形で、リクエストを受け付けたときに取得できます。
```typescript
export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
        ...
    },
}
```

`Request`オブジェクト自体は、Cloudflare の仕様ということではなく、W3C の WebAPI 仕様にある、[Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) で規定されているものです。
では、この`Request`オブジェクトを使って、各種パラメータを取得する方法について見ていきましょう。

### メソッドの判定

メソッドは、`request.method`で取得可能です。

```typescript
const method = request.method; // GETやPOSTが返ります
```

### クエリーパラメータ

クエリーパラメータというのは、たとえば`https://example.com/?key1=value1&key2=value2`のようなURLの中の、`key1`とか`key2`に当たる部分です。
これらを取得するには、まずは`Response`オブジェクトから`URL`オブジェクトを取り出し、`URL`オブジェクトの`searchParams`パラメータで取得できます。具体的には以下のようになります。

```typescript
const url = new URL(request.url);
const searchParams = url.searchParams;
```

取り出した結果は、`for`でループさせることで、keyとvalueに分解することができます。

```typescript
for (const [key, value] of searchParams) console.info(key, value);
// key1 value1
// key2 value2
```

### パスの取得

パスというのは、たとえば`https://example.com/path1?key1=value1&key2=value2`のようなURLの中の、`path1`に当たる部分です。
パスを取得するには、`URL`オブジェクトの`pathname`パラメータを取得すればよいのですが、パスは / を使って何段にも分けることが可能なので、以下のようにして最初のパスを取得します。

```typescript
const pathArray = url.pathname.split('/');
const path = pathArray[1];
```

:::note warn
pathArray配列の１番目（pathArray[0]）にはドメイン名が入るので、パスは2番目以降に入る点に注意してください。
:::

### Body パラメータの取得

`POST`メソッドでは、Body 部にデータを入れてリクエストすることがあります。
たとえば、JSON 形式で Body 部に指定されたパラメータを取得するには以下のように記述します。

```typescript
const data = await request.json();
```

`Request`オブジェクトの`json()`メソッドは非同期で`Promise`を返すようになっていますので、`await`を使って処理を行っています。

## パラメータをバリデーションする

リクエストを受け付けたあとに、パラメータの整合性チェックを行うケースがほとんどだと思います。
これらをよりシンプルにするために、サンプルコード内では、`value-schema`ライブラリを使っています。

https://github.com/shimataro/value-schema

`value-schema`では、たとえば以下のようにパラメータのスキーマを定義することができます。

```typescript
const RequestBodySchema = {
	key1: vs.string(),
	key2: vs.number({
		ifUndefined: 0,
	}),
	key3: vs.string({
		only: ['a', 'b', 'c'],
	}),
}
```

これは以下の制約を意味しています。
- key1という名前の`String`型パラメータが必須です。
- key2については`Number`型の任意のパラメータになりますが、値が設定されていない場合は0として処理します。
- key3は、`a`、`b`もしくは`c`のいずれかが`String`型として指定されている必要があります。

それで、このスキーマを実際の Body パラメータでチェックするのが、以下のコードになります。

```typescript
// Bodyパラメータのチェック
const data = vs.applySchemaObject(RequestBodySchema, await request.json(), errorHandler, () => {
	// エラーが発生するたびに errorHandler が呼ばれ、すべてのエラーチェックが終わったらerrorを投げる
	throw new Error(`👺 Parameter error. ${errors.join(', ')}`);
});
```

`value-schema`の`applySchemaObject`メソッドを使うと、スキーマとの整合性を確認することができます。引数の１番目がスキーマ定義、２番目は比較対象となるJSONオブジェクト、エラーがある場合はパラメータごとに第３引数（このケースでは、`errorHandler`）が呼ばれ、すべてのエラーがチェックし終わったときに第４引数がコールバックされます。

:::note info
第３、第４引数はエラーがない場合はコールされません。
:::

:::note warn
クエリーパラメータも同様の方法でバリデーションが可能ですが、`url.searchParams`では JSON オブジェクトが返らないので、このサンプルコード内ではバリデーション用に JSON を生成してからチェックしています。
:::

# まとめ

まだまだ Workers 初心者ですが、このプロダクトは大変おもしろいので、これからも色々と触っていきたいと思います。

---
### KDDIウェブコミュニケーションズ（KWC）について

https://cloudapi.kddi-web.com
残念ながら2023年5月1日をもって、KWCは10年間に渡るTwilioのリセール事業を終了いたしましたが、引き続きCPaaS市場において、過去の実績や知見をもとによりよいサービスを提供するべく活動を続けております。
また引き続き、Twilioのサポートについても、CPaaS Care（仮）という形で無償にて対応させていただくこととなります。詳しくは上記サイトをご覧ください。

自己紹介|　
:--|:--
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/b51a8b88-abd5-ce5e-195a-de3564ba2ae6.png" height='200' width='150'/>|高橋克己（Katsumi Takahashi） 自称「<font color='red'>□い芸人</font>」<br />グローバル・インターネット・ジャパン株式会社　代表取締役<br />株式会社KDDIウェブコミュニケーションズ　コミュニケーションDX事業部エバンジェリスト<br /><br />2001年より大手通信事業者の法人サービスの教育に携わり、企業における電話のしくみや重要性を研究。2016年よりKWCにジョインし、Twilioを使ったスマートコミュニケーションの普及活動を精力的に行っている。<br />2015 Hall of Doers<br />2019 Twilio Champions