Skip to Content
Response & Errors

Response and Errors

Procedures wrap Next.js route handlers, so they can return Response objects. They can also return plain objects and throw exceptions, which Vovk.ts turns into Response objects.

For type inference in the TypeScript client, these snippets are the same:

// ... export default class HelloController { @get() static helloWorld() { return { hello: 'world' }; } }
import { NextResponse } from 'next/server'; // ... export default class HelloController { @get() static helloWorld() { return NextResponse.json({ hello: 'world' }); } }
// ... export default class HelloController { @get() static helloWorld() { return new Response(JSON.stringify({ hello: 'world' }), { headers: { 'Content-Type': 'application/json' }, }) as unknown as { hello: string }; } }

Response Headers

Static Response Headers

Every HTTP decorator takes custom response headers in its second argument.

// ... export default class UserController { @put('do-something', { headers: { 'x-hello': 'world' } }) static async doSomething(/* ... */) { /* ... */ } }

Enable CORS headers with the cors: true option.

// ... export default class UserController { @put('do-something', { cors: true }) static async doSomething(/* ... */) { /* ... */ } }

For .auto() endpoints, pass cors and headers as the only argument.

// ... export default class UserController { @put.auto({ cors: true, headers: { 'x-hello': 'world' } }) static async doSomething(/* ... */) { /* ... */ } }

Dynamic Response Headers

Set headers per response with a NextResponse or Response object.

import { NextResponse } from 'next/server'; // ... export default class UserController { @put('do-something') static async doSomething() { return NextResponse.json({ hello: 'world' }, { headers: { 'x-hello': 'world' } }); } }

redirect and notFound

To redirect, or to render the not-found page, use the Next.js functions from next/navigation.

import { get } from 'vovk'; import { redirect, notFound } from 'next/navigation'; export default class UserController { @get('redirect') static async redirect() { // ... if something redirect('/some-other-endpoint'); } @get('not-found') static async notFound() { // ... if something notFound(); } }

Both functions throw an error, so you need no return statement; their return type is never. The same goes for forbidden() and unauthorized(), which Next.js allows only with experimental.authInterrupts in next.config; without it they throw a plain error, which answers 500. Vovk.ts rethrows these errors, so Next.js answers them.

In a JSON Lines stream, after the response has started, they end the stream with an error line instead. notFound(), forbidden() and unauthorized() send their status: 404, 403 or 401. redirect() is sent as any other error: “Internal server error” in production.

See the Next.js documentation .

File Downloads

For a file attachment, return a Web API Response with the right headers.

// ... export default class DownloadController { @get('csv-report') static async downloadCSV() { return new Response(csvString, { headers: { 'Content-Type': 'text/csv', 'Content-Disposition': `attachment; filename=report.csv`, }, }); } }

The toDownloadResponse helper builds this response for you. It takes the file content as a Blob | File | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array> | string, and an optional object with filename, type and headers.

import { toDownloadResponse } from 'vovk'; // ... export default class DownloadController { @get('csv-report') static async downloadCSV() { return toDownloadResponse(csvString, { filename: 'report.csv', type: 'text/csv', headers: { 'x-hello': 'world' } }); } }

Errors

Throw HTTP errors with the HttpException class, in a syntax inspired by NestJS. It takes three arguments: an HTTP status from HttpStatus, a message and an optional cause object.

import { HttpException, HttpStatus } from 'vovk'; // ... static async updateUser(/* ... */) { // ... throw new HttpException(HttpStatus.BAD_REQUEST, 'Something went wrong'); }

The TypeScript client rethrows them with the same interface.

import { UserRPC } from '@/client'; import { HttpException } from 'vovk'; // ... try { const updatedUser = await UserRPC.updateUser(/* ... */); } catch (e) { console.log(e instanceof HttpException); // true const err = e as HttpException; console.log(err.message, err.statusCode); }

Other errors, such as Error, count as an HttpException with status 500. In production, their message and cause stay on the server, and the client gets “Internal server error”. The same goes for an HttpException with status 0, which an RPC module throws when a call gets no response or fails client-side validation. To pass such a failure on, catch it and throw an HttpException with a status.

import { HttpException, HttpStatus } from 'vovk'; // ... static async updateUser(/* ... */) { // ... throw new Error('Something went wrong'); // 500 }

The third argument, cause, adds context:

throw new HttpException(HttpStatus.BAD_REQUEST, 'Something went wrong', { hello: 'World' });

An error from a vovk client that a handler calls, such as an OpenAPI mixin or another segment, is an HttpException with the upstream status. It reaches the handler’s caller with the upstream status, message and body, in production too. Catch it if the caller shouldn’t see that.

HttpStatus Enum

The values of the HttpStatus enum:

export enum HttpStatus { NULL = 0, // for client-side errors, such as client-side validation CONTINUE = 100, SWITCHING_PROTOCOLS = 101, PROCESSING = 102, EARLYHINTS = 103, OK = 200, CREATED = 201, ACCEPTED = 202, NON_AUTHORITATIVE_INFORMATION = 203, NO_CONTENT = 204, RESET_CONTENT = 205, PARTIAL_CONTENT = 206, AMBIGUOUS = 300, MOVED_PERMANENTLY = 301, FOUND = 302, SEE_OTHER = 303, NOT_MODIFIED = 304, TEMPORARY_REDIRECT = 307, PERMANENT_REDIRECT = 308, BAD_REQUEST = 400, UNAUTHORIZED = 401, PAYMENT_REQUIRED = 402, FORBIDDEN = 403, NOT_FOUND = 404, METHOD_NOT_ALLOWED = 405, NOT_ACCEPTABLE = 406, PROXY_AUTHENTICATION_REQUIRED = 407, REQUEST_TIMEOUT = 408, CONFLICT = 409, GONE = 410, LENGTH_REQUIRED = 411, PRECONDITION_FAILED = 412, PAYLOAD_TOO_LARGE = 413, URI_TOO_LONG = 414, UNSUPPORTED_MEDIA_TYPE = 415, REQUESTED_RANGE_NOT_SATISFIABLE = 416, EXPECTATION_FAILED = 417, I_AM_A_TEAPOT = 418, MISDIRECTED = 421, UNPROCESSABLE_ENTITY = 422, FAILED_DEPENDENCY = 424, PRECONDITION_REQUIRED = 428, TOO_MANY_REQUESTS = 429, INTERNAL_SERVER_ERROR = 500, NOT_IMPLEMENTED = 501, BAD_GATEWAY = 502, SERVICE_UNAVAILABLE = 503, GATEWAY_TIMEOUT = 504, HTTP_VERSION_NOT_SUPPORTED = 505, }

Proxy Response

JSON Proxy

A proxy endpoint gets data from another server with fetch and returns its Response, which Next.js sends as is. For the right type on the client, cast the return type:

import { get } from 'vovk'; export default class ProxyController { @get('greeting') static getHello() { return fetch('https://vovk.dev/api/hello/greeting.json') as unknown as { greeting: string }; } }

View live example on examples.vovk.dev » 

The client then infers that type:

import { ProxyRPC } from '@/client'; // ... const { greeting } = await ProxyRPC.getHello();

Or keep the server return type as Response, and set the type on the client method:

import { ProxyRPC } from '@/client'; // ... const { greeting } = await ProxyRPC.getHello<{ greeting: string }>();

Blob Proxy

If you keep the Response type, the client resolves the call to a Response object. Use this for files or binary data.

import { get } from 'vovk'; export default class ProxyController { @get('pdf-proxy') static getPdf() { return fetch('https://example.com/example.pdf'); } }
import { ProxyRPC } from '@/client'; // ... const response = await ProxyRPC.getPdf(); const buffer = await response.arrayBuffer(); const blob = new Blob([buffer], { type: 'application/pdf' }); // ...
Last updated on