fix(TypedServer): Improve error handling in server startup and response buffering. Validate configuration for reload injections, wrap file watching and TypedSocket initialization in try/catch blocks, enhance client notification and stop procedures, and ensure proper Buffer conversion in the proxy handler.

This commit is contained in:
Philipp Kunz 2025-03-16 12:02:49 +00:00
parent 0dcb9edcbe
commit d5f7fbbb9a
4 changed files with 137 additions and 52 deletions

View File

@ -1,5 +1,15 @@
# Changelog
## 2025-03-16 - 3.0.70 - fix(TypedServer)
Improve error handling in server startup and response buffering. Validate configuration for reload injections, wrap file watching and TypedSocket initialization in try/catch blocks, enhance client notification and stop procedures, and ensure proper Buffer conversion in the proxy handler.
- Add validation to throw error if reload script is enabled without a serve directory
- Wrap file watching and TypedSocket initialization in try/catch to prevent crashes during startup
- Update the reload function to safely notify clients and handle notification errors
- Enhance the stop procedure to aggregate cleanup tasks with error handling
- Ensure consistent conversion of response bodies to Buffer in HandlerProxy with fallback when undefined
- Include fallback hash generation in createServeDirHash for error resilience
## 2025-03-16 - 3.0.69 - fix(servertools)
Fix compression stream creation returns, handler proxy buffer conversion, and sitemap URL concatenation

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@api.global/typedserver',
version: '3.0.69',
version: '3.0.70',
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
}

View File

@ -81,6 +81,7 @@ export class TypedServer {
public lastReload: number = Date.now();
public ended = false;
constructor(optionsArg: IServerOptions) {
const standardOptions: IServerOptions = {
port: 3000,
@ -117,19 +118,32 @@ export class TypedServer {
}
res.write(this.lastReload.toString());
res.end();
break;
default:
res.status(404);
res.write('Unknown request type');
res.end();
break;
}
})
);
this.server.addRoute(
'/typedrequest',
new servertools.HandlerTypedRouter(this.typedrouter)
)
);
}
/**
* inits and starts the server
*/
public async start() {
// Validate essential configuration before starting
if (this.options.injectReload && !this.options.serveDir) {
throw new Error(
'You set to inject the reload script without a serve dir. This is not supported at the moment.'
);
}
if (this.options.serveDir) {
this.server.addRoute(
'/*',
@ -154,7 +168,7 @@ export class TypedServer {
console.log('injected typedserver script.');
responseArg.responseContent = Buffer.from(fileString);
} else if (this.options.injectReload) {
console.log('Could not insert typedserver script');
console.log('Could not insert typedserver script - no <head> tag found');
}
}
const headers = responseArg.headers;
@ -166,6 +180,7 @@ export class TypedServer {
headers,
path: responseArg.path,
responseContent: responseArg.responseContent,
travelData: responseArg.travelData,
};
},
serveIndexHtmlDefault: true,
@ -173,40 +188,44 @@ export class TypedServer {
preferredCompressionMethod: this.options.preferredCompressionMethod,
})
);
} else if (this.options.injectReload) {
throw new Error(
'You set to inject the reload script without a serve dir. This is not supported at the moment.'
);
}
if (this.options.watch && this.options.serveDir) {
this.smartchokInstance = new plugins.smartchok.Smartchok([this.options.serveDir]);
await this.smartchokInstance.start();
(await this.smartchokInstance.getObservableFor('change')).subscribe(async () => {
try {
this.smartchokInstance = new plugins.smartchok.Smartchok([this.options.serveDir]);
await this.smartchokInstance.start();
(await this.smartchokInstance.getObservableFor('change')).subscribe(async () => {
await this.createServeDirHash();
this.reload();
});
await this.createServeDirHash();
this.reload();
});
await this.createServeDirHash();
} catch (error) {
console.error('Failed to initialize file watching:', error);
// Continue without file watching rather than crashing
}
}
// lets start the server
await this.server.start();
this.typedsocket = await plugins.typedsocket.TypedSocket.createServer(
this.typedrouter,
this.server
);
try {
this.typedsocket = await plugins.typedsocket.TypedSocket.createServer(
this.typedrouter,
this.server
);
// lets setup typedrouter
this.typedrouter.addTypedHandler<interfaces.IReq_GetLatestServerChangeTime>(
new plugins.typedrequest.TypedHandler('getLatestServerChangeTime', async (reqDataArg) => {
return {
time: this.lastReload,
};
})
);
// console.log('open url in browser');
// await plugins.smartopen.openUrl(`http://testing.git.zone:${this.options.port}`);
// lets setup typedrouter
this.typedrouter.addTypedHandler<interfaces.IReq_GetLatestServerChangeTime>(
new plugins.typedrequest.TypedHandler('getLatestServerChangeTime', async () => {
return {
time: this.lastReload,
};
})
);
} catch (error) {
console.error('Failed to initialize TypedSocket:', error);
// Continue without WebSocket support rather than crashing
}
}
/**
@ -214,33 +233,80 @@ export class TypedServer {
*/
public async reload() {
this.lastReload = Date.now();
for (const connectionArg of await this.typedsocket.findAllTargetConnectionsByTag(
'typedserver_frontend'
)) {
const pushTime =
this.typedsocket.createTypedRequest<interfaces.IReq_PushLatestServerChangeTime>(
if (!this.typedsocket) {
console.warn('TypedSocket not initialized, skipping client notifications');
return;
}
try {
const connections = await this.typedsocket.findAllTargetConnectionsByTag('typedserver_frontend');
for (const connection of connections) {
const pushTime = this.typedsocket.createTypedRequest<interfaces.IReq_PushLatestServerChangeTime>(
'pushLatestServerChangeTime',
connectionArg
connection
);
pushTime.fire({
time: this.lastReload,
});
pushTime.fire({
time: this.lastReload,
});
}
} catch (error) {
console.error('Failed to notify clients about reload:', error);
}
}
public async stop() {
/**
* Stops the server and cleans up resources
*/
public async stop(): Promise<void> {
this.ended = true;
await this.server.stop();
await this.typedsocket.stop();
if (this.smartchokInstance) {
await this.smartchokInstance.stop();
const stopWithErrorHandling = async (
stopFn: () => Promise<unknown>,
componentName: string
): Promise<void> => {
try {
await stopFn();
} catch (err) {
console.error(`Error stopping ${componentName}:`, err);
}
};
const tasks: Promise<void>[] = [];
// Stop server
if (this.server) {
tasks.push(stopWithErrorHandling(() => this.server.stop(), 'server'));
}
// Stop TypedSocket
if (this.typedsocket) {
tasks.push(stopWithErrorHandling(() => this.typedsocket.stop(), 'TypedSocket'));
}
// Stop file watcher
if (this.smartchokInstance) {
tasks.push(stopWithErrorHandling(() => this.smartchokInstance.stop(), 'file watcher'));
}
await Promise.all(tasks);
}
/**
* Calculates a hash of the served directory for cache busting
*/
public async createServeDirHash() {
const serveDirHash = await plugins.smartfile.fs.fileTreeToHash(this.options.serveDir, '**/*');
this.serveHash = serveDirHash;
console.log('Current ServeDir hash: ' + serveDirHash);
this.serveDirHashSubject.next(serveDirHash);
try {
const serveDirHash = await plugins.smartfile.fs.fileTreeToHash(this.options.serveDir, '**/*');
this.serveHash = serveDirHash;
console.log('Current ServeDir hash: ' + serveDirHash);
this.serveDirHashSubject.next(serveDirHash);
} catch (error) {
console.error('Failed to create serve directory hash:', error);
// Use a timestamp-based hash as fallback
const fallbackHash = Date.now().toString(16).slice(-6);
this.serveHash = fallbackHash;
console.log('Using fallback hash: ' + fallbackHash);
this.serveDirHashSubject.next(fallbackHash);
}
}
}
}

View File

@ -40,12 +40,21 @@ export class HandlerProxy extends Handler {
}
}
let responseToSend: Buffer = proxiedResponse.body;
// Ensure body exists and convert it to Buffer consistently
let responseToSend: Buffer;
// Remove incorrect type check that expects responseToSend to be a string
// Instead, ensure it's a Buffer for consistent handling
if (!Buffer.isBuffer(responseToSend)) {
responseToSend = Buffer.from(responseToSend.toString());
if (proxiedResponse.body !== undefined && proxiedResponse.body !== null) {
if (Buffer.isBuffer(proxiedResponse.body)) {
responseToSend = proxiedResponse.body;
} else if (typeof proxiedResponse.body === 'string') {
responseToSend = Buffer.from(proxiedResponse.body);
} else {
// Handle other types (like objects) by JSON stringifying them
responseToSend = Buffer.from(JSON.stringify(proxiedResponse.body));
}
} else {
// Provide a default empty buffer if body is undefined/null
responseToSend = Buffer.from('');
}
if (optionsArg && optionsArg.responseModifier) {