Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions apps/api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# GasGuard API

Nest.js backend handling remote scan requests with timeout protection.

## Features

- **Timeout Protection**: Prevents runaway scans from degrading system performance
- **Configurable Timeouts**: Set max execution time via environment variables
- **Worker Thread Support**: Optional worker-based scanning for additional isolation
- **Clear Error Messages**: Graceful failure with descriptive timeout errors

## Configuration

Set the maximum execution time for scans via environment variable:

```bash
SCAN_MAX_EXECUTION_TIME_MS=30000 # 30 seconds (default)
```

## Usage

### Basic Scan Endpoint

```bash
POST /scan
Content-Type: application/json

{
"contractCode": "contract code here...",
"timeoutMs": 30000 # optional, overrides default
}
```

### Response

**Success:**
```json
{
"scanId": "scan_1234567890_abc123",
"status": "completed",
"findings": [],
"executionTime": 1234567890
}
```

**Timeout Error:**
```json
{
"statusCode": 408,
"message": "Scan exceeded maximum execution time. Scan exceeded maximum execution time of 30000ms",
"error": {
"code": "SCAN_TIMEOUT",
"message": "Scan exceeded maximum execution time of 30000ms",
"scanId": "scan_1234567890_abc123",
"timeoutMs": 30000
}
}
```

## Implementation Details

### Promise.race Approach (Default)

The `ScanService` uses `Promise.race()` to enforce timeouts:

```typescript
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(timeoutError), timeoutMs);
});
const result = await Promise.race([scanPromise, timeoutPromise]);
```

### Worker Thread Approach (Optional)

For additional isolation, use `WorkerScanService` which runs scans in worker threads:

- Prevents blocking the main event loop
- Automatic cleanup on timeout via `worker.terminate()`
- Better resource isolation

## Error Handling

All timeout errors include:
- `code`: "SCAN_TIMEOUT"
- `message`: Descriptive error message
- `scanId`: Unique identifier for tracking
- `timeoutMs`: The timeout value that was exceeded
168 changes: 168 additions & 0 deletions apps/api/TIMEOUT_IMPLEMENTATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Timeout Implementation Guide

## Overview

This implementation prevents runaway scans from degrading system performance by enforcing maximum execution time limits per scan operation.

## Implementation Approaches

### 1. Promise.race Approach (Primary - ScanService)

**Location:** `src/scan/scan.service.ts`

**How it works:**
- Uses `Promise.race()` to compete a scan promise against a timeout promise
- If the timeout promise resolves first, the scan is terminated with a clear error
- Lightweight and doesn't require additional processes

**Usage:**
```typescript
const result = await scanService.executeScan(contractCode, {
timeoutMs: 30000 // optional, defaults to config value
});
```

**Error Handling:**
```typescript
try {
const result = await scanService.executeScan(contractCode);
} catch (error) {
if (error.code === 'SCAN_TIMEOUT') {
console.error(`Scan timed out after ${error.timeoutMs}ms`);
}
}
```

### 2. Worker Thread Approach (Optional - WorkerScanService)

**Location:** `src/scan/worker-scan.service.ts`

**How it works:**
- Runs scans in isolated worker threads
- Automatically terminates workers that exceed timeout
- Provides better isolation and prevents blocking the main event loop
- More resource-intensive but safer for long-running operations

**Usage:**
```typescript
// Add WorkerScanService to ScanModule providers
const result = await workerScanService.executeScanInWorker(contractCode, {
timeoutMs: 30000
});
```

**Benefits:**
- Complete isolation from main process
- Automatic cleanup via `worker.terminate()`
- Prevents event loop blocking

## Configuration

### Environment Variables

Set the default maximum execution time:

```bash
# .env or .env.local
SCAN_MAX_EXECUTION_TIME_MS=30000 # 30 seconds (default)
```

### Per-Scan Override

You can override the timeout for individual scans:

```typescript
// Use custom timeout for this specific scan
await scanService.executeScan(contractCode, {
timeoutMs: 60000 // 60 seconds
});
```

## Error Response Format

### Timeout Error Structure

```typescript
{
code: 'SCAN_TIMEOUT',
message: 'Scan exceeded maximum execution time of 30000ms',
scanId: 'scan_1234567890_abc123',
timeoutMs: 30000
}
```

### HTTP Response (408 Request Timeout)

```json
{
"statusCode": 408,
"message": "Scan exceeded maximum execution time. Scan exceeded maximum execution time of 30000ms",
"error": {
"code": "SCAN_TIMEOUT",
"message": "Scan exceeded maximum execution time of 30000ms",
"scanId": "scan_1234567890_abc123",
"timeoutMs": 30000
}
}
```

## Integration Points

### Current Implementation

1. **ScanService** (`src/scan/scan.service.ts`)
- Primary service using Promise.race
- Used by ScanController

2. **WorkerScanService** (`src/scan/worker-scan.service.ts`)
- Optional worker-based implementation
- Can be added to ScanModule if needed

3. **ScanController** (`src/scan/scan.controller.ts`)
- HTTP endpoint: `POST /scan`
- Handles timeout errors with 408 status code
- Validates input before starting scan

## Testing Timeout Behavior

### Unit Test Example

See `src/scan/scan.service.spec.ts` for examples of:
- Testing successful scans
- Testing timeout scenarios
- Testing custom timeout values

### Manual Testing

To test timeout behavior, you can simulate a long-running scan:

```typescript
// In performScan method, add a delay longer than timeout
await new Promise(resolve => setTimeout(resolve, 40000)); // 40 seconds
// Then call with 30 second timeout - should fail
```

## Best Practices

1. **Set Reasonable Defaults**: 30 seconds is a good default, but adjust based on:
- Average contract complexity
- System resources
- User expectations

2. **Log Timeouts**: All timeouts are logged with scan ID for debugging

3. **Monitor Timeout Frequency**: If many scans timeout, consider:
- Increasing default timeout
- Optimizing scan algorithms
- Using worker threads for better isolation

4. **Graceful Degradation**: Timeout errors are clearly communicated to users

## Done Criteria ✅

- ✅ Scans exceeding limit fail gracefully with clear error
- ✅ Uses Node.js process timeout (Promise.race)
- ✅ Provides worker constraints option (WorkerScanService)
- ✅ Configurable via environment variables
- ✅ Per-scan timeout override supported
- ✅ Comprehensive error handling and logging
30 changes: 30 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@gasguard/api",
"version": "1.0.0",
"description": "GasGuard API - Nest.js backend handling remote scan requests",
"main": "dist/main.js",
"scripts": {
"build": "tsc",
"start": "node dist/main.js",
"start:dev": "ts-node-dev --respawn --transpile-only src/main.ts",
"test": "jest"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/config": "^3.0.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@types/jest": "^29.0.0",
"jest": "^29.0.0",
"ts-jest": "^29.0.0",
"ts-node": "^10.9.0",
"ts-node-dev": "^2.0.0",
"typescript": "^5.0.0"
}
}
14 changes: 14 additions & 0 deletions apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ScanModule } from './scan/scan.module';

@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.local', '.env'],
}),
ScanModule,
],
})
export class AppModule {}
25 changes: 25 additions & 0 deletions apps/api/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);

// Enable validation
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);

// Enable CORS if needed
app.enableCors();

const port = process.env.PORT || 3000;
await app.listen(port);
console.log(`GasGuard API is running on: http://localhost:${port}`);
}

bootstrap();
39 changes: 39 additions & 0 deletions apps/api/src/scan/interfaces/scan.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Result of a contract scan operation
*/
export interface ScanResult {
scanId: string;
status: 'completed' | 'failed' | 'timeout';
findings: ScanFinding[];
executionTime: number;
metadata?: {
contractSize?: number;
rulesApplied?: string[];
};
}

/**
* Individual finding from a scan
*/
export interface ScanFinding {
ruleId: string;
severity: 'error' | 'warning' | 'info';
message: string;
location?: {
line: number;
column: number;
file?: string;
};
suggestion?: string;
}

/**
* Error structure for scan failures
*/
export interface ScanError {
code: 'SCAN_TIMEOUT' | 'SCAN_ERROR' | 'INVALID_INPUT';
message: string;
scanId?: string;
timeoutMs?: number;
details?: unknown;
}
Loading