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:
		
							
								
								
									
										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) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user