Customizing using Javascript
Tailcall provides a light-weight JS runtime to modify requests and resolve with custom responses. The runtime is not a full-fledged Node.js environment and has no access to the file system or the network. It is designed to be used for simple request/response modifications.
Getting Started
To leverage this customization, JavaScript functions must be created in a JavaScript file and linked with the main configuration file using the @link directive. There are two primary ways to achieve this:
- Define an
onRequest
property with the JS function name in the http directive. - Define it in the upstream directive, which acts as a global middleware for all requests.
If you specify a onRequest
handler for both http
and upstream
the http
one will always take precedence over the global onRequest
handler.
The function serves as middleware, allowing for the interception and modification of the request, as well as the production of artificial responses. Here is a simple example of a worker.js
file with a function named foo
, which takes a request
object as an argument, logs the request, and returns the original request without any modifications.
function foo({request}) {
console.log(`${request.method} ${request.uri.path}`)
return {request}
}
Once you have a worker file ready, link that file to the GraphQL configuration using the @link
directive and define the onRequest property.
schema
@link(type: Script, src: "./worker.js")
@upstream(onRequest: "foo") {
query: Query
}
Now, you can start the server using the usual start command. Requests made to the GraphQL server will now be intercepted by the worker and logged to the console.
Modify Request
You can modify the request by returning a request
object from the onRequest
function. Below is an example where we are modifying the request to add a custom header.
function onRequest({request}) {
request.headers["x-custom-header"] = "Hello, Tailcall!"
return {request}
}
Create Response
You can respond with custom responses by returning a response
object from the onRequest
function. Below is an example where we are responding with a custom response for all requests that start with https://api.example.com
.
function onRequest({request}) {
if (request.uri.path.startsWith("https://api.example.com")) {
return {
response: {
status: 200,
headers: {
"content-type": "application/json"
},
body: JSON.stringify({message: "Hello, Tailcall!"})
}
}
}
else {
return {request}
}
Response Redirect
Sometimes you might want to redirect the request to a different URL. You can do this by returning a response
object with a status
of 301
or 302
and a Location
header. The following example redirects all requests to https://example.com
to https://tailcall.com
.
function onRequest({request}) {
if (request.uri.path.startsWith("https://example.com")) {
return {
response: {
status: 301,
headers: {
Location: "https://tailcall.com",
},
},
}
} else {
return {request}
}
}
The new request that's created as a result of the redirect will not be intercepted by the worker.
Schema
The onRequest
function takes a single argument that contains the request object. The return value of the onRequest
function can be a request
object, or a response
object. It can not be null or undefined.
Request
The request object has the following shape:
type Request = {
method: string
uri: {
path: string
query?: {[key: string]: string}
scheme: "Http" | "Https"
host?: string
port?: number
}
headers: {[key: string]: string}
body?: string
}
By default the headers field will be empty in most cases, unless headers are whitelisted via the allowedHeaders setting in @upstream
.
The http filter doesn't have access to the request's body, hence you can't directly modify the body of an outgoing request. This is more of a design choice than a limitation we have made to ensure that developers don't misuse this API to write all kind of business logic in Tailcall.
As an escape hatch you can pass the request body as a query param instead of an actual request body and read in the JS.
The modified request that's returned from the above onRequest
function can optionally provide the body. This body is used by Tailcall as the request body while making the upstream request.
Response
The response object has the following shape:
type Response = {
status: number
headers: {[key: string]: string}
body?: string
}