Skip to content

Commit

Permalink
feat: working cross-engine proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
drochetti committed Oct 8, 2023
1 parent 862f7a1 commit 31a50ff
Show file tree
Hide file tree
Showing 10 changed files with 24,058 additions and 14,404 deletions.
11 changes: 11 additions & 0 deletions apps/demo-express-app/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,22 @@

import express from 'express';
import * as path from 'path';
import * as falProxy from '@fal-ai/serverless-proxy/express';
import { configDotenv } from 'dotenv';
import cors from 'cors';

configDotenv({ path: './env.local' });

const app = express();

// Middlewares
app.use('/assets', express.static(path.join(__dirname, 'assets')));
app.use(express.json());

// fal.ai client proxy
app.all(falProxy.route, cors(), falProxy.handler);

// Your API endpoints
app.get('/api', (req, res) => {
res.send({ message: 'Welcome to demo-express-app!' });
});
Expand Down
3 changes: 2 additions & 1 deletion apps/demo-nextjs-app/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { useMemo, useState } from 'react';
// @snippet:start(client.config)
fal.config({
requestMiddleware: fal.withProxy({
targetUrl: '/api/_fal/proxy',
targetUrl: '/api/_fal/proxy', // the built-int nextjs proxy
// targetUrl: 'http://localhost:3333/api/_fal/proxy', // or your own external proxy
}),
});
// @snippet:end
Expand Down
2 changes: 1 addition & 1 deletion libs/client/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@fal-ai/serverless-client",
"description": "The fal serverless JS/TS client",
"version": "0.2.1",
"version": "0.3.0",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
3 changes: 1 addition & 2 deletions libs/client/src/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,7 @@ export async function run<Input, Output>(
const response = await fetch(url, {
method,
headers: requestHeaders,
mode: 'same-origin',
credentials: 'same-origin',
mode: 'cors',
body:
method !== 'get' && options.input
? JSON.stringify(options.input)
Expand Down
2 changes: 1 addition & 1 deletion libs/proxy/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fal-ai/serverless-proxy",
"version": "0.1.0",
"version": "0.3.0",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
24 changes: 23 additions & 1 deletion libs/proxy/src/express.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
import type { RequestHandler } from 'express';
import { DEFAULT_PROXY_ROUTE, handleRequest } from './index';

/**
* The default Express route for the fal.ai client proxy.
*/
export const route = DEFAULT_PROXY_ROUTE;

/**
* The Express route handler for the fal.ai client proxy.
*
* @param request The Express request object.
* @param response The Express response object.
* @param next The Express next function.
*/
export const handler: RequestHandler = async (request, response, next) => {
handleRequest(request, response);
await handleRequest({
id: 'express',
method: request.method,
respondWith: (status, data) =>
typeof data === 'string'
? response.status(status).send(data)
: response.status(status).json(data),
getHeaders: () => request.headers,
getHeader: (name) => request.headers[name],
removeHeader: (name) => response.removeHeader(name),
sendHeader: (name, value) => response.setHeader(name, value),
getBody: () => JSON.stringify(request.body),
});
next();
};
75 changes: 44 additions & 31 deletions libs/proxy/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { Request, Response } from 'express';

export const TARGET_URL_HEADER = 'x-fal-target-url';

export const DEFAULT_PROXY_ROUTE = '/api/_fal/proxy';
Expand All @@ -9,30 +7,45 @@ const FAL_KEY_ID = process.env.FAL_KEY_ID || process.env.NEXT_PUBLIC_FAL_KEY_ID;
const FAL_KEY_SECRET =
process.env.FAL_KEY_SECRET || process.env.NEXT_PUBLIC_FAL_KEY_SECRET;

export interface ProxyBehavior {
id: string;
method: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
respondWith(status: number, data: string | any): void;
getHeaders(): Record<string, string | string[] | undefined>;
getHeader(name: string): string | string[] | undefined;
removeHeader(name: string): void;
sendHeader(name: string, value: string): void;
getBody(): string | undefined;
}

/**
* Utility to get a header value as `string` from a Headers object.
*
* @private
* @param request the Next request object.
* @param key the header key.
* @param request the header value.
* @returns the header value as `string` or `undefined` if the header is not set.
*/
function getHeader(request: Request, key: string): string | undefined {
const headerValue = request.headers[key.toLowerCase()];
if (Array.isArray(headerValue)) {
return headerValue[0];
function singleHeaderValue(
value: string | string[] | undefined
): string | undefined {
if (value === undefined) {
return undefined;
}
if (Array.isArray(value)) {
return value[0];
}
return headerValue;
return value;
}

/**
* Clean up headers that should not be forwarded to the proxy.
* @param request the Next request object.
* @param behavior The proxy implementation.
*/
function cleanUpHeaders(request: Request) {
delete request.headers['origin'];
delete request.headers['referer'];
delete request.headers[TARGET_URL_HEADER];
function cleanUpHeaders(behavior: ProxyBehavior) {
behavior.removeHeader('origin');
behavior.removeHeader('referer');
behavior.removeHeader(TARGET_URL_HEADER);
}

function getFalKey(): string | undefined {
Expand All @@ -55,58 +68,58 @@ function getFalKey(): string | undefined {
* @param response the Next response object.
* @returns Promise<any> the promise that will be resolved once the request is done.
*/
export const handleRequest = async (request: Request, response: Response) => {
const targetUrl = getHeader(request, TARGET_URL_HEADER);
export const handleRequest = async (behavior: ProxyBehavior) => {
const targetUrl = singleHeaderValue(behavior.getHeader(TARGET_URL_HEADER));
if (!targetUrl) {
response.status(400).send(`Missing the ${TARGET_URL_HEADER} header`);
behavior.respondWith(400, `Missing the ${TARGET_URL_HEADER} header`);
return;
}
if (targetUrl.indexOf('fal.ai') === -1) {
response.status(412).send(`Invalid ${TARGET_URL_HEADER} header`);
behavior.respondWith(412, `Invalid ${TARGET_URL_HEADER} header`);
return;
}

cleanUpHeaders(request);
cleanUpHeaders(behavior);

const falKey = getFalKey();
if (!falKey) {
response.status(401).send('Missing fal.ai credentials');
behavior.respondWith(401, 'Missing fal.ai credentials');
return;
}

// pass over headers prefixed with x-fal-*
const headers: Record<string, string | string[] | undefined> = {};
Object.keys(request.headers).forEach((key) => {
Object.keys(behavior.getHeaders()).forEach((key) => {
if (key.toLowerCase().startsWith('x-fal-')) {
headers[key.toLowerCase()] = request.headers[key];
headers[key.toLowerCase()] = behavior.getHeader(key);
}
});

const res = await fetch(targetUrl, {
method: request.method,
method: behavior.method,
headers: {
...headers,
authorization: getHeader(request, 'authorization') ?? `Key ${falKey}`,
authorization:
singleHeaderValue(behavior.getHeader('authorization')) ??
`Key ${falKey}`,
accept: 'application/json',
'content-type': 'application/json',
'x-fal-client-proxy': '@fal-ai/serverless-nextjs',
'x-fal-client-proxy': `@fal-ai/serverless-proxy/${behavior.id}`,
},
body:
request.method?.toUpperCase() === 'GET'
? undefined
: JSON.stringify(request.body),
behavior.method?.toUpperCase() === 'GET' ? undefined : behavior.getBody(),
});

// copy headers from res to response
res.headers.forEach((value, key) => {
response.setHeader(key, value);
behavior.sendHeader(key, value);
});

if (res.headers.get('content-type') === 'application/json') {
const data = await res.json();
response.status(res.status).json(data);
behavior.respondWith(res.status, data);
return;
}
const data = await res.text();
response.status(res.status).send(data);
behavior.respondWith(res.status, data);
};
30 changes: 20 additions & 10 deletions libs/proxy/src/nextjs.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
import type { NextApiHandler } from 'next/types';
import { DEFAULT_PROXY_ROUTE, handleRequest } from './index';

/**
* The default Next API route for the fal.ai client proxy.
*/
export const PROXY_ROUTE = DEFAULT_PROXY_ROUTE;

/**
* The Next API route handler for the fal.ai client proxy.
*
* @param request
* @param response
* @returns
* @param request the Next API request object.
* @param response the Next API response object.
* @returns a promise that resolves when the request is handled.
*/
export const handler: NextApiHandler = async (request, response) => {
// TODO: right now we know the handleRequest function is relies on
// properties that are common to both NextApiRequest and Request
// but we should make sure that is the case by creating our own
// interface as a contract for the minimal request/response properties
// that handleRequest needs.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return handleRequest(request as any, response as any);
return handleRequest({
id: 'nextjs',
method: request.method,
respondWith: (status, data) =>
typeof data === 'string'
? response.status(status).send(data)
: response.status(status).json(data),
getHeaders: () => request.headers,
getHeader: (name) => request.headers[name],
removeHeader: (name) => response.removeHeader(name),
sendHeader: (name, value) => response.setHeader(name, value),
getBody: () => JSON.stringify(request.body),
});
};
Loading

0 comments on commit 31a50ff

Please sign in to comment.