or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

directory-listing.mdindex.mdplugin-registration.mdreply-decorators.md
tile.json

reply-decorators.mddocs/

Reply Decorators

Manual file serving methods added to FastifyReply for programmatic file operations with custom headers, download behavior, and flexible root path specification.

Capabilities

sendFile Method

Send a file from the configured root directory or a custom root path with optional response customization.

/**
 * Send a file from static root or custom root path
 * @param filename - Name of file to send relative to root
 * @param rootPath - Optional custom root path (string) or options object
 * @param options - Optional send options to override defaults
 * @returns FastifyReply instance for chaining
 */
sendFile(filename: string, rootPath?: string): FastifyReply;
sendFile(filename: string, options?: SendOptions): FastifyReply;
sendFile(filename: string, rootPath?: string, options?: SendOptions): FastifyReply;

Usage Examples:

// Basic usage - serve from configured root
fastify.get('/file', (req, reply) => {
  return reply.sendFile('document.pdf');
});

// Custom root path
fastify.get('/upload', (req, reply) => {
  return reply.sendFile('image.jpg', path.join(__dirname, 'uploads'));
});

// With options to override defaults
fastify.get('/cached', (req, reply) => {
  return reply.sendFile('asset.css', { 
    maxAge: '1d',
    immutable: true,
    cacheControl: true
  });
});

// Custom root with options
fastify.get('/temp', (req, reply) => {
  return reply.sendFile('temp.txt', '/tmp', {
    maxAge: 0,
    cacheControl: false
  });
});

// Async handler pattern
fastify.get('/async-file', async (req, reply) => {
  return reply.sendFile('data.json');
});

download Method

Send a file with content-disposition header to trigger download behavior in browsers.

/**
 * Send file with content-disposition header for download
 * @param filepath - Path to file relative to root
 * @param filename - Optional custom filename for download (string) or options object
 * @param options - Optional send options to override defaults
 * @returns FastifyReply instance for chaining
 */
download(filepath: string, options?: SendOptions): FastifyReply;
download(filepath: string, filename?: string): FastifyReply;
download(filepath: string, filename?: string, options?: SendOptions): FastifyReply;

Usage Examples:

// Basic download - uses original filename
fastify.get('/download', (req, reply) => {
  return reply.download('reports/annual-report.pdf');
});

// Custom download filename
fastify.get('/report', (req, reply) => {
  return reply.download('reports/2023-annual.pdf', 'Annual Report 2023.pdf');
});

// Download with options
fastify.get('/export', (req, reply) => {
  return reply.download('exports/data.csv', {
    maxAge: 0, // Don't cache downloads
    cacheControl: false
  });
});

// Custom filename with options
fastify.get('/backup', (req, reply) => {
  return reply.download(
    'backups/db-backup.sql',
    'database-backup.sql',
    { 
      dotfiles: 'allow',
      maxAge: 0 
    }
  );
});

// Dynamic download based on parameters
fastify.get('/download/:type/:id', (req, reply) => {
  const { type, id } = req.params;
  const filename = `${type}-${id}.pdf`;
  const downloadName = `${type.toUpperCase()}_${id}.pdf`;
  
  return reply.download(filename, downloadName);
});

Send Options

Configuration options for customizing file sending behavior, overriding plugin defaults.

/**
 * Options for customizing file sending behavior
 */
interface SendOptions {
  /** Enable/disable accepting ranged requests */
  acceptRanges?: boolean;
  
  /** Enable/disable Content-Type header setting */
  contentType?: boolean;
  
  /** Enable/disable Cache-Control header */
  cacheControl?: boolean;
  
  /** How to handle dotfiles: 'allow' | 'deny' | 'ignore' */
  dotfiles?: 'allow' | 'deny' | 'ignore';
  
  /** Enable/disable etag generation */
  etag?: boolean;
  
  /** File extensions to attempt when no extension in URL */
  extensions?: string[];
  
  /** Enable immutable directive in Cache-Control */
  immutable?: boolean;
  
  /** Index file names or false to disable */
  index?: string[] | string | false;
  
  /** Enable/disable Last-Modified header */
  lastModified?: boolean;
  
  /** Cache max-age in ms or time string */
  maxAge?: string | number;
}

Usage Examples:

// Aggressive caching for assets
const assetOptions = {
  maxAge: '1y',
  immutable: true,
  cacheControl: true,
  etag: true
};

fastify.get('/assets/:file', (req, reply) => {
  return reply.sendFile(req.params.file, assetOptions);
});

// No caching for dynamic content
const dynamicOptions = {
  maxAge: 0,
  cacheControl: false,
  etag: false,
  lastModified: false
};

fastify.get('/api/export', (req, reply) => {
  return reply.download('temp/export.json', dynamicOptions);
});

// Security-focused options
const secureOptions = {
  dotfiles: 'deny',
  extensions: false, // Don't try extensions
  index: false // Don't serve index files
};

fastify.get('/secure/:file', (req, reply) => {
  return reply.sendFile(req.params.file, secureOptions);
});

Error Handling

Handle errors during file serving operations.

Usage Examples:

// Handle file not found
fastify.get('/maybe-file', (req, reply) => {
  try {
    return reply.sendFile('might-not-exist.txt');
  } catch (error) {
    if (error.code === 'ENOENT') {
      return reply.code(404).send({ error: 'File not found' });
    }
    throw error;
  }
});

// With custom error handler
fastify.setErrorHandler((error, request, reply) => {
  if (error.statusCode === 404) {
    reply.code(404).sendFile('404.html');
  } else {
    reply.send(error);
  }
});

// Conditional file serving
fastify.get('/conditional/:file', async (req, reply) => {
  const { file } = req.params;
  
  // Check if file exists before sending
  try {
    await fs.access(path.join(__dirname, 'public', file));
    return reply.sendFile(file);
  } catch {
    return reply.code(404).send({ error: 'File not available' });
  }
});

Advanced Usage Patterns

Complex scenarios combining multiple features.

Usage Examples:

// Conditional root paths
fastify.get('/media/:type/:file', (req, reply) => {
  const { type, file } = req.params;
  const rootMap = {
    'images': path.join(__dirname, 'images'),
    'videos': path.join(__dirname, 'videos'),
    'docs': path.join(__dirname, 'documents')
  };
  
  const root = rootMap[type];
  if (!root) {
    return reply.code(404).send({ error: 'Invalid media type' });
  }
  
  return reply.sendFile(file, root, {
    maxAge: type === 'docs' ? '1h' : '1d'
  });
});

// User-specific file serving
fastify.get('/user/:userId/files/:filename', async (req, reply) => {
  const { userId, filename } = req.params;
  
  // Verify user access
  if (req.user.id !== userId && !req.user.isAdmin) {
    return reply.code(403).send({ error: 'Access denied' });
  }
  
  const userRoot = path.join(__dirname, 'user-files', userId);
  return reply.download(filename, `${req.user.name}-${filename}`, {
    root: userRoot,
    maxAge: 0
  });
});

// Content-type override
fastify.get('/api-docs/:file', (req, reply) => {
  const { file } = req.params;
  
  if (file.endsWith('.json')) {
    reply.type('application/json');
  } else if (file.endsWith('.yaml')) {
    reply.type('application/x-yaml');
  }
  
  return reply.sendFile(file, path.join(__dirname, 'api-docs'), {
    contentType: false // Let us handle content-type manually
  });
});