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:
parent
0dcb9edcbe
commit
d5f7fbbb9a
10
changelog.md
10
changelog.md
@ -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
|
||||
|
||||
|
@ -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.'
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user