メインコンテンツまでスキップ

非同期ワークフロー

非同期 API は、生成完了を待たずに requestId を即座に受け取り、後から Webhook かポーリングで結果を回収する方式です。同期エンドポイントの 60 秒タイムアウトを避けられるため、大量生成やバックグラウンド処理に向きます。このガイドでは、並列実行・キューベース・バッチ処理の3パターンと、リトライや部分失敗の扱い方を説明します。

非同期APIとは​

Re:port Flow APIの一部のエンドポイントは非同期処理をサポートしています:

API同期非同期
単一PDF生成/file/sync/single/file/async/single
複数PDF生成/file/sync/multiple/file/async/multiple

完了をどう待つ?​

非同期 API は 202 Accepted と requestId を返した時点ではまだ生成中です。完了は次のどちらかで確認します。

方法向いている場面
状態 API(GET /v1/file/status/{requestId})をポーリング受信用の公開 URL を用意できない、バッチ処理の中で待ちたい
Webhook 通知(file.completed。生成リクエストで webhookUrl を指定した場合は、その宛先に file.failed も届く)サーバーで受信できる。待ち時間を無駄にしたくない

状態 API でポーリングする​

  1. 生成 API のレスポンスから requestId を取り出す
  2. GET /v1/file/status/{requestId} を呼ぶ
  3. status が queued / processing なら、レスポンスの Retry-After ヘッダー(秒)だけ待って 2 に戻る
  4. completed なら files[].downloadPath からダウンロードする。複数生成で一部だけ失敗したときも completed で、失敗分は failedFiles に入るので、あわせて記録する。failed なら error.code で理由を判定する
const API_BASE = 'https://api.re-port-flow.com';

async function waitAndDownload(requestId) {
let notFoundRetries = 0;
for (;;) {
let res;
try {
res = await axios.get(`${API_BASE}/v1/file/status/${requestId}`, {
headers: { 'appkey': process.env.APP_KEY },
});
} catch (e) {
// 生成直後の 404 は、ジョブの記録を保存できなかった生成中のリクエストのことがある。2 回まで確かめ直す
if (e.response?.status === 404 && notFoundRetries < 2) {
notFoundRetries += 1;
await new Promise(resolve => setTimeout(resolve, 3000));
continue;
}
// 429(状態 API は 300 req/min)は Retry-After 秒待って確かめ直す
if (e.response?.status === 429) {
const waitMs = Number(e.response.headers['retry-after'] ?? 60) * 1000;
await new Promise(resolve => setTimeout(resolve, waitMs));
continue;
}
throw e;
}

if (res.data.status === 'completed') {
// 1 件ずつ順に取得する(ダウンロードは 100 req/min。複数生成は最大 100 件あり、一度に送ると 429 になりうる)
const pdfs = [];
for (const file of res.data.files) {
for (;;) {
try {
const pdf = await axios.get(`${API_BASE}${file.downloadPath}`, {
headers: { 'appkey': process.env.APP_KEY },
responseType: 'arraybuffer',
});
pdfs.push(pdf);
break;
} catch (e) {
// 429 は Retry-After 秒待って同じファイルを取り直す
if (e.response?.status !== 429) throw e;
const retrySec = Number(e.response.headers['retry-after'] ?? 60);
await new Promise(resolve => setTimeout(resolve, retrySec * 1000));
}
}
}
// 複数生成の一部だけが失敗しても status は completed。失敗分は failedFiles に入るので呼び出し側へ渡す
// (理由は完了から 24 時間で取得できなくなるので、ここで記録しておく)
return { pdfs, failedFiles: res.data.failedFiles ?? [] };
}
if (res.data.status === 'failed') {
throw new Error(`${res.data.error.code}: ${res.data.error.message}`);
}

const waitSec = Number(res.headers['retry-after'] ?? 2);
await new Promise(resolve => setTimeout(resolve, waitSec * 1000));
}
}
  • 状態 API の Rate Limit は 300 req/min(Workspace単位)で、ダウンロード API の 100 req/min とは別枠です。
  • 複数生成のファイルをまとめて受け取るなら、1 件ずつではなく ZIP 一括ダウンロード(GET /v1/file/download/{requestId})を 1 回呼ぶ方法もあります(PDF のダウンロード)。
  • 非同期生成の状態は完了・失敗から 24 時間保持されます。24 時間を過ぎると、単一生成の失敗は 404 REQUEST_NOT_FOUND になり、複数生成ですべて失敗したリクエストは status: "failed"(error.code は FILE_GENERATION_FAILED)だけが返ってファイルごとの理由(failedFiles)は取得できなくなります。失敗理由はそれまでに記録してください(状態の保持期間)。
  • 実運用では上の例にタイムアウト(全体の待ち時間の上限)を足してください。実装例は PDF生成ガイド にあります。
ダウンロード API の 404 で完了を推測しない

以前は生成中のファイルをダウンロードすると 404 が返ったため、「404 の間は生成中」とみなすポーリングが必要でした。現在のダウンロード API は、生成中なら 409 FILE_NOT_READY(Retry-After 付き)、生成失敗なら 422 FILE_GENERATION_FAILED を返します。404 は、ID の誤り、別のワークスペースの ID、失敗から 24 時間を過ぎた単一生成、または出力が削除されたことを意味します(生成中でもジョブの記録が無い場合は 404)。完了待ちには状態 API を使ってください。

非同期処理のメリット​

1. タイムアウト回避​

同期APIは60秒でタイムアウトしますが、非同期APIは長時間の処理に対応できます。

// ❌ 同期API: 大量データで60秒を超える可能性
const pdf = await generateSyncPDF(largeData);

// ✅ 非同期API: タイムアウトなし
const { url } = await generateAsyncPDF(largeData);

2. バックグラウンド処理​

ユーザーを待たせずに処理を実行できます。

// ユーザーリクエストを即座に受け付け
app.post('/api/generate-report', async (req, res) => {
// 非同期でPDF生成をキック
const { requestId, url, fileId } = await startPDFGeneration(req.body);

// 即座にレスポンス
res.json({
message: 'PDF の生成を開始しました',
requestId,
fileId,
// url はアプリ画面(出力履歴)へのリンク。要ログインで、ファイルの
// 直接ダウンロードURLではない。ファイルは requestId / fileId を使って
// ダウンロードAPIから取得する(認証方法は認証ガイドを参照)
outputsUrl: url
});

// 完了後にメール通知(バックグラウンド)
notifyWhenComplete(fileId, req.user.email);
});

3. 大量処理の並列化​

複数のPDF生成を並列実行できます。

// 100件のPDFを並列生成
const requests = invoices.map(invoice =>
generateAsyncPDF({
designId: 'invoice-template',
content: invoice
})
);

const results = await Promise.all(requests);
console.log(`${results.length}件のPDFを生成しました`);

大量処理のパターン​

パターン1: 並列実行(小〜中規模)​

async function generateBulkPDFs(items, concurrency = 5) {
const chunks = chunkArray(items, concurrency);
const results = [];

for (const chunk of chunks) {
const promises = chunk.map(item =>
generateAsyncPDF({
designId: 'template-id',
content: item
})
);

const chunkResults = await Promise.all(promises);
results.push(...chunkResults);

console.log(`${results.length}/${items.length} 完了`);
}

return results;
}

function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}

// 使用例: 100件を5件ずつ並列処理
const invoices = [...]; // 100件
const results = await generateBulkPDFs(invoices, 5);

パターン2: キューベース(大規模)​

import Queue from 'bull';

// Redisベースのキューを作成
const pdfQueue = new Queue('pdf-generation', {
redis: {
host: 'localhost',
port: 6379
}
});

// ワーカー設定
pdfQueue.process(async (job) => {
const { designId, content } = job.data;

// PDF生成API呼び出し
const result = await generateAsyncPDF({ designId, content });

// 進捗更新
await job.progress(100);

return result;
});

// ジョブ追加
async function enqueuePDFGeneration(items) {
const jobs = items.map(item =>
pdfQueue.add({
designId: 'template-id',
content: item
}, {
attempts: 3,
backoff: {
type: 'exponential',
delay: 2000
}
})
);

return Promise.all(jobs);
}

// イベント監視
pdfQueue.on('completed', (job, result) => {
console.log(`ジョブ ${job.id} 完了:`, result);
});

pdfQueue.on('failed', (job, err) => {
console.error(`ジョブ ${job.id} 失敗:`, err);
});

パターン3: バッチ処理(超大規模)​

// 1000件以上の場合は複数PDFの一括生成を使用
async function generateMassiveBulk(items) {
const batchSize = 100; // APIの上限に合わせる
const batches = chunkArray(items, batchSize);

const results = [];

for (const [index, batch] of batches.entries()) {
console.log(`バッチ ${index + 1}/${batches.length} 処理中...`);

// 複数PDF一括生成API
const { url, files } = await generateMultiplePDFsAsync({
designId: 'template-id',
contents: batch.map(item => ({
fileName: `${item.id}.pdf`,
params: item
}))
});

results.push({ batchIndex: index, url, files });

// レート制限対策で少し待機
if (index < batches.length - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}

return results;
}

エラーハンドリング​

リトライロジック​

async function generateWithRetry(params, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await generateAsyncPDF(params);
} catch (error) {
const isLastAttempt = attempt === maxRetries - 1;

if (isServerError(error) && !isLastAttempt) {
const delay = 1000 * Math.pow(2, attempt);
console.log(`リトライ ${attempt + 1}/${maxRetries} (${delay}ms後)`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}

throw error;
}
}
}

// 500 / 502 / 503 だけ再送する。504 / 524 などは受付済みのことがあり、再送するとジョブが二重にできうる
function isServerError(error) {
return [500, 502, 503].includes(error.response?.status);
}

部分的な失敗の処理​

/file/async/multiple で一部のファイルだけ失敗した場合、状態 API は status: "completed" のまま失敗したファイルを failedFiles に入れて返します。Webhook では、成功したファイルが file.completed で届きます。失敗したファイルの file.failed(error.code: "PARTIAL_FAILURE")は、生成リクエストで webhookUrl を指定した場合だけ、その宛先に届きます(Webhook 通知)。

複数の単一生成リクエストを並べる場合は、リクエストごとに成否を集計します。

async function generateWithFallback(items) {
const results = [];
const errors = [];

for (const item of items) {
try {
const result = await generateAsyncPDF({
designId: 'template-id',
content: item
});
results.push({ item, result, status: 'success' });
} catch (error) {
console.error(`アイテム ${item.id} の生成失敗:`, error);
errors.push({ item, error, status: 'failed' });
}
}

return {
results,
errors,
summary: {
total: items.length,
success: results.length,
failed: errors.length
}
};
}

Python実装例​

import asyncio
import aiohttp
from typing import List, Dict, Any

async def generate_pdf_async(session: aiohttp.ClientSession, params: Dict[str, Any]) -> Dict:
"""非同期でPDFを生成"""
url = "https://api.re-port-flow.com/v1/file/async/single"
headers = {
'appkey': params['app_key'],
'Content-Type': 'application/json'
}
data = {
'designId': params['design_id'],
'version': params['version'],
'content': params['content']
}

async with session.post(url, headers=headers, json=data) as response:
response.raise_for_status()
return await response.json()

async def generate_bulk_pdfs(
items: List[Dict],
app_key: str,
design_id: str,
version: int = 1,
concurrency: int = 5,
):
"""複数PDFを並列生成(Session を 1 つ共有して接続プールを効かせる)"""
semaphore = asyncio.Semaphore(concurrency)

async with aiohttp.ClientSession() as session:
async def generate_with_limit(item):
async with semaphore:
return await generate_pdf_async(session, {
'app_key': app_key,
'design_id': design_id,
'version': version,
'content': item,
})

tasks = [generate_with_limit(item) for item in items]
return await asyncio.gather(*tasks)

# 使用例
items = [...] # 生成対象データ
results = asyncio.run(generate_bulk_pdfs(
items,
app_key='your-app-key',
design_id='template-id',
version=1,
concurrency=10,
))

ベストプラクティス​

  1. 適切な並列度を設定: サーバー負荷とレスポンス速度のバランスを取る
  2. Retry-After に従う: 状態 API・429・409 のポーリング間隔はヘッダーの秒数に合わせる。500 / 502 / 503 のリトライは指数バックオフ(504 / 524 などそれ以外の 5xx は、受付済みのことがあるので自動では再送しない)
  3. タイムアウトを設定: 無限待機を防ぐ
  4. エラーをログ: 失敗したアイテムを追跡
  5. 進捗を表示: ユーザーに状況を伝える

次のステップ​