Skip to main content

One min read
Josh Twist

Length: 2 minutes

In this post we pickup where left off in this post Gateway over SaaS? and take our AirTable API and make it work directly with a form POST from a website.

It even has a honeypot field to filter out simple bots 馃憦

Here's the form post code from JSFiddle

<form method="POST" action="<YOUR ZUPLO API URL HERE>">
<input type="text" name="name" value="" />
<input type="text" name="email" value="" />
<input type="text" style="display:hidden" name="hp" value="" />
<button>submit</button>
</form>

One min read
Josh Twist

Length: 2 minutes

This one's a little extra. Zuplo is so programmable you can use it in ways you've never considered for a gateway... a gateway over SaaS APIs - like AirTable.

In this example we use the Event Planning Template.

And here's the code in our request handler

import { ZuploContext, ZuploRequest, environment } from "@zuplo/runtime";

export default async function (request: ZuploRequest, context: ZuploContext) {
const body = await request.json();

const data = {
records: [
{
fields: {
Name: body.name,
Email: body.email,
},
},
],
};

const response = await fetch(environment.ATTENDEES_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${environment.API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});

if (!response.ok) {
return new Response("Error calling AirTable!", {
status: 500,
});
}
return new Response("Success", {
status: 200,
});
}

2 min read
Josh Twist

Length: 3 minutes

One of my favorite features of Zuplo is the ability to build custom policies. Here we create a custom policy to archive every request to Amazon's S3 storage. Here's the code in our archive-request.ts module:

import { ZuploContext, ZuploRequest, environment } from "@zuplo/runtime";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

type MyPolicyOptionsType = {
myOption: any;
};
export default async function (
request: ZuploRequest,
context: ZuploContext,
options: MyPolicyOptionsType,
policyName: string
) {
context.log.info(environment.AWS_SECRET_ACCESS_KEY);

const s3Client = new S3Client({ region: "us-east-2" });
const file = `${Date.now()}-${crypto.randomUUID()}.req.txt`;

const clone = request.clone();

const uploadParams = {
Bucket: "request-storage",
Key: file,
Body: await clone.text(),
};

const data = await s3Client.send(new PutObjectCommand(uploadParams));

return request;
}

Note, the code above will update S3 in serial with invoking your API, which will increase the latency of your API. However, you can also do this asynchronously, as follows:

import { ZuploContext, ZuploRequest, environment } from "@zuplo/runtime";
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";

type MyPolicyOptionsType = {
myOption: any;
};
export default async function (
request: ZuploRequest,
context: ZuploContext,
options: MyPolicyOptionsType,
policyName: string
) {
context.log.info(environment.AWS_SECRET_ACCESS_KEY);

const s3Client = new S3Client({ region: "us-east-2" });
const file = `${Date.now()}-${crypto.randomUUID()}.req.txt`;

const clone = request.clone();

const uploadParams = {
Bucket: "request-storage",
Key: file,
Body: await clone.text(),
};

const dataPromise = s3Client.send(new PutObjectCommand(uploadParams));
// This tells the runtime to not shutdown until that promise is complete
context.waitUntil(dataPromise);

return request;
}

One min read
Josh Twist

Length: 2 minutes

Bad inputs can easily break your API. Stop bad form before it even hits your API with Zuplo. In this demo we show how you can add JSON validation to an API without touching your original API.

We use JSON Schema with our JSON Validation policy. Here's the schema:

{
"title": "Person",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The person's first name.",
"pattern": "\\S+ \\S+"
},
"company": {
"type": "string"
}
},

"additionalProperties": false,
"required": ["name"]
}

Easy peasy.

One min read
Josh Twist

Length: 2 minutes

We continue with the example from this post and add smart routing based on claims in the token.

Here's the function handler we create to do the smart routing

import { ZuploContext, ZuploRequest } from "@zuplo/runtime";

export default async function (request: ZuploRequest, context: ZuploContext) {
const data = request.user.data;
if (data["https://example.com/claim1/"] === "this-is-a-claim"){
return fetch("https://example.com");
}
else {
return fetch(`https://ecommerce-legacy.zuplo.io/objects?type=products&id=${request.params.productId});
}
}

One min read
Josh Twist

Length: 2 minutes

Here we show how easy it is to add JWT authentication to an API using the Zuplo gateway. We extend the demo from this post.

There was no code required for this sample, it's that easy 馃檶

One min read
Josh Twist

Length: 3 minutes

In this video we see just how programmable the Zuplo gateway is by going full tilt and building a simple API using Function Handlers 馃く

We also look at wildcard routes like so /(.*).

Here's the code from the echo API we create

import { ZuploContext, ZuploRequest } from "@zuplo/runtime";

export default async function (request: ZuploRequest, context: ZuploContext) {
const { url, query } = request;

const body = await request.text();

return { url, query, body };
}

One min read
Josh Twist

Length: 3 minutes

In this video we show you just how easy it is to proxy an API with Zuplo and add rate-limiting. We also prettify the URL of our ugly backend to make things feel a little more modern and current.

There was no code in this demo but we did do a URL rewrite as follows

https://ecommerce-legacy.zuplo.io/objects?type=products&id=${params.productId}

3 min read
Josh Twist

The problem

CloudFlare is one of our hosting partners at Zuplo and we recently encountered a very tricky error that occurred intermittently and was not throwing an exception catchable by our logging framework. Our code wouldn鈥檛 even execute the finally blocks when we countered this. The only clue was deep in the CloudFlare Workers streaming logs - that looked something like this...

"outcome": "exception",
"scriptName": null,
"exceptions": [
{
"name": "Error",
"message": "The script will never generate a response.",
"timestamp": 1643142701479
}
]
//...

We were confident there was no code path in our runtime that wouldn鈥檛 return a response, even if an exception is thrown. It didn鈥檛 seem like we were running out of memory or some other critical exception - so what was going on?

A request-aware event-loop

With a little help from the CloudFlare team, we learned that CloudFlare workers do some slightly unusual things with the JavaScript event loop.

Specifically, they track which requests contributed each chunk of work to the event loop. This allows them to do some magic - like allocate each console.log to the correct request, even in a single-threaded isolate that is handling many requests at once.

However, if you use global, module-level, or static variables you can confuse how Workers track things 鈥 leading to some weird results.

Request flow

In this case, let us imagine you have two requests A and B being handled by the same worker instance. If A creates a Promise and stores this in an area of state accessible by request B you鈥檒l hit this issue if request B awaits (blocks on) that while it is still pending. Note - it won鈥檛 fail if the Promise is already fulfilled.

A simple repro

You can recreate the bug with the following code.

addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request));
});

// we're going to store the promise here and use it across requests
let blocker;

async function handleRequest(request) {
if (!blocker) {
// block for 20s
blocker = sleep(20000);
}
await blocker;
blocker = undefined;
return new Response("ok");
}

function sleep(delay) {
const p = new Promise((res, rej) => {
setTimeout(res, delay);
});
return p;
}

If you deploy this and hit CloudFlare hard enough to get two requests on the same instance while streaming logs you鈥檒l see the error happen. You can do this pretty easily with apache benchmark:

ab -n 10 -c 10 https://your.worker.dev/

Beware hidden examples Of course, the challenge with bugs like these is they often only show up intermittently on a busy server. We actually encountered this inside one of the libraries we use at Zuplo: Jose - specifically this code (hopefully fixed by the time you鈥檙e reading this):

async reload() {
if (!this._pendingFetch) {
this._pendingFetch = fetchJwks(
this._url,
this._timeoutDuration,
this._options
)
.then((json) => {
if (!isJWKSLike(json)) {
throw new JWKSInvalid("JSON Web Key Set malformed");
}

this._jwks = { keys: json.keys };
this._cooldownStarted = Date.now();
this._pendingFetch = undefined;
})
.catch((err: Error) => {
this._pendingFetch = undefined;
throw err;
});
}
}

await this._pendingFetch();

It鈥檚 easy to see now, how this could trigger the aggressive termination of the event loop of a request that attempts to await that _pendingFetch at the end.

We hope this helps! Thanks to Erwin van der Koogh at CloudFlare for his help getting to the bottom of this.