api-client/packages/hoppscotch-backend/src/mock-server/mock-server.controller.ts
Anwarul Islam e607f9db24
feat(common): mock server ui improvements (#5532)
- Update active state styles for better visibility in the mock server.
- BE updates catered to improving content type handling in the mock server.
- Introduced a `disableMockServerInPersonalWorkspace` platform-level feature flag.
- Remove inactive keyboard shorthand nudges from the Mock server dashboard context menu.

---

Co-authored-by: mirarifhasan <arif.ishan05@gmail.com>
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
2025-10-30 15:17:16 +05:30

139 lines
4.5 KiB
TypeScript

import {
Controller,
All,
Req,
Res,
HttpStatus,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { MockServerService } from './mock-server.service';
import { MockServerLoggingInterceptor } from './mock-server-logging.interceptor';
import * as E from 'fp-ts/Either';
import { MockRequestGuard } from './mock-request.guard';
import { MockServer } from '@prisma/client';
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
/**
* Mock server controller with dual routing support:
* 1. Subdomain pattern: mock-server-id.mock.hopp.io/product
* 2. Route pattern: backend.hopp.io/mock/mock-server-id/product
*
* The MockRequestGuard handles extraction of mock server ID from both patterns
* The MockServerLoggingInterceptor handles logging of all requests
*/
@UseGuards(ThrottlerBehindProxyGuard)
@Controller({ path: 'mock' })
export class MockServerController {
constructor(private readonly mockServerService: MockServerService) {}
@All('*path')
@UseGuards(MockRequestGuard)
@UseInterceptors(MockServerLoggingInterceptor)
async handleMockRequest(@Req() req: Request, @Res() res: Response) {
// Mock server ID and info are attached by the guard
const mockServerId = (req as any).mockServerId as string;
const mockServer = (req as any).mockServer as MockServer;
if (!mockServerId) {
return res.status(HttpStatus.NOT_FOUND).json({
error: 'Not found',
message: 'Mock server ID not found',
});
}
const method = req.method;
// Get clean path (removes /mock/mock-server-id prefix for route-based pattern)
const path = MockRequestGuard.getCleanPath(
req.path || '/',
mockServer.subdomain,
);
// Extract query parameters
const queryParams = req.query as Record<string, string>;
// Extract request headers (convert to lowercase for case-insensitive matching)
const requestHeaders: Record<string, string> = {};
Object.keys(req.headers).forEach((key) => {
const value = req.headers[key];
if (typeof value === 'string') {
requestHeaders[key.toLowerCase()] = value;
} else if (Array.isArray(value)) {
requestHeaders[key.toLowerCase()] = value[0];
}
});
try {
const result = await this.mockServerService.handleMockRequest(
mockServer,
path,
method,
queryParams,
requestHeaders,
);
if (E.isLeft(result)) {
return res.status(HttpStatus.NOT_FOUND).json({
error: 'Endpoint not found',
message: result.left,
});
}
const mockResponse = result.right;
// Set response headers if any
if (mockResponse.headers) {
try {
const headers = JSON.parse(mockResponse.headers);
Object.keys(headers).forEach((key) => {
console.log('Setting header:', key, headers[key]);
res.setHeader(key, headers[key]);
});
} catch (error) {
console.error('Error parsing response headers:', error);
}
}
// Add delay if specified
if (mockServer.delayInMs && mockServer.delayInMs > 0) {
await new Promise((resolve) =>
setTimeout(resolve, mockServer.delayInMs),
);
}
// Only set Content-Type if not already set
if (!res.getHeader('Content-Type')) {
let defaultContentType = 'text/plain';
// Check if body is a string and try to parse it to determine content type
if (typeof mockResponse.body === 'string') {
try {
JSON.parse(mockResponse.body);
// If parsing succeeds, it's JSON
defaultContentType = 'application/json';
} catch {
// If parsing fails, it's plain text
defaultContentType = 'text/plain';
}
} else if (typeof mockResponse.body === 'object') {
// If it's already an object, it's JSON
defaultContentType = 'application/json';
}
res.setHeader('Content-Type', defaultContentType);
}
// Security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
// Send response
return res.status(mockResponse.statusCode).send(mockResponse.body);
} catch (error) {
console.error('Error handling mock request:', error);
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
error: 'Internal server error',
message: 'Failed to process mock request',
});
}
}
}