danmaru
v2.0.0
Published
No dependency, convenience HTTTP/HTTPS server wrapper
Readme
danmaru
No dependency, convenience HTTTP/HTTPS server wrapper in TypeScript. For complete TypeScript code see https://github.com/sdrsdr/danmaru
How to use
import * as http from 'http';
import {compose,log_all,codes} from 'danmaru';
const log=log_all();
const server= new http.Server();
compose(
server,[
{prefix:"/hello_json?", do: (req,resp)=>{
let who=req.full_url.searchParams.get('who')??"<who param not found in searchParams>";
log.mark("Hello "+who+" of JSON!");
resp.json_response(codes.OK,{say:'Hello '+who+' of JSON',method:req.method});
}}
],{log}
);
server.listen(1234,()=>{
log.mark("danmaru HTTP server started at port 1234");
});
compose
function compose(
server:http_Server|https_Server,
http_actions:http_action_t[],
options?:options_t
):boolean ;Hookup server to handle requests via http_actions various options like log functions, global maximal body size and global method filters goes in options
http_action_t
interface http_action_t {
prefix:string;
do:http_action_cb;
m?:string[]; //allowed methods
max_body_size?:number; //if not set max_body_size from options or MAX_BODY_SIZE will be enforced
exact_match?:boolean; //default false; if true prefix must exact-match
error_catcher?:error_catcher_cb; //catch failing requests
}Array of this interface goes in compose to describe the urls handled by the server.
prefixis the start of the url to match. You can have sameprefixin thecomposearray multiple time with different allowed methods and differentdo. The match lookup folows array order ofcomposehttp_actionsparam. First match handles the request completely.dois the callback that will genrate the responcemIs optional string array that explicityl allows only mentioned HTTP methods. If this is not set the method filter fromcompose'soptinskick in. It is possible to have multiplehttp_actionwith sameprefixbut differentmfilters and different domax_body_sizeis optional limiter for http resuest body size once the request matches. You can set global limit incompose'soptions. There is some hard coded limit if none is set explicityl.exact_matchis assumed false if missing. This changes how the incoming request url is matched againsthttp_action. It is possible to have multiplehttp_actionwith sameprefixbut differentexact_matchand different doerror_catchera callback that will receive all request about to be rejected by the library.
options_t
interface options_t {
log?:logger_t;
indexer?:http_action_cb; //default handler; if not set a 404 is send back
auto_headers?:OutgoingHttpHeaders; //this headers are preset for sending for each response
auto_handle_OPTIONS?:boolean; //default is false. Just do resp.simple_response(200) and return for all non 404 urls and OPTIONS method (passing the content of auto_headers to the browser)
max_body_size?:number; //in characters if not set MAX_BODY_SIZE will be enforced
allowed_methods?:string[]; // default is no-filtering;
catch_to_500?:boolean; //catch exceptions in http_action_t.do, log in err, respond with error code 500 (if possible)
error_catcher?:error_catcher_cb; //catch failing requests,
//usually send errors are loged in error level, with this option the log is lowered to debug level. Typical uisage send_error_ignore:['ERR_STREAM_DESTROYED']
send_error_ignore?:string[];
}options for compose:
loglog4js inspired interface to logging facility to be used in the server. Some helper functions are provided:function log_all():logger_tandfunction log_none():logger_tto help with faster setup.indexeris a url handler for unmatched requests. It can do custom 404 pages or index direcotories somehow. It's all up to you. The helper functionhttp_action_goneis provided that just returns cacheable410 GONEresponses.auto_headersheaders specified here will be automatically send with every response. Primary target is...auto_headers:{"Access-Control-Allow-Origin":"*"}, ...auto_handle_OPTIONSis of by default. If set totrueit allows for unmatched requests withOPTIONSmethod to be automatically replied with200 OKplus theauto_headersto allow some browsers to check for CORS headers.max_body_sizeglobal request body size limiter. if not set MAX_BODY_SIZE will be enforced.allowed_methodsstates what HTTP methods your server will handle. If you want to handle onlyGETand/orPOSTrequest you can state so here and keephttp_actionparam ofcomposecleaner or you can intermix all HTTP method filtering to you likecatch_to_500makes error handling easy in.docallback by puting atry {...} catch ( ... ) { ... }block so an exception thrown in your code will cause internal server error code 500 to be send. To proper catch async errors you need to return the (hidden) promise from from the first async function called. See the example in http_action_cb belowerror_catchera callback that will receive all request about to be rejected by the library.
http_action_cb
interface http_action_cb {
(req:CompleteIncomingMessage, resp:SimpleServerResponse) :void|Promise<any>;
}The http request handling callback in form
function handle_a_request(req:CompleteIncomingMessage, resp:SimpleServerResponse) {
...
}or
async function handle_a_request(req:CompleteIncomingMessage, resp:SimpleServerResponse) {
...
}this is the do in http_action_t and indexer in options_t
as usual use the information in req to craft a resp
if you're using catch_to_500 option and mixing sync and async functions make really sure you're returing the tail call functions. For example if you're using a authentication middleware you should do some strict returns:
type chain_cb_t=(req: CompleteIncomingMessage, resp: SimpleServerResponse, auth_artefact:string)=>void|Promise<any>;
//chack auth call chain if auth ok
function auth (req: CompleteIncomingMessage, resp: SimpleServerResponse, chain:chain_cb_t){
let auth_artefact=req......
....
return chain(req,resp,auth_artefact)
}
async function dothis(req: CompleteIncomingMessage, resp: SimpleServerResponse, auth_artefact:string) {
await ....
}
...
compose(
server,
[
...
{prefix:"/api/dothis?", do: (req,resp)=>{return auth(req,resp,dothis)}},
{prefix:"/api/dothat?", do: (req,resp)=>{return auth(req,resp,dothat)}},
....
],
options
);CompleteIncomingMessage
The danmaru request object extending from NodeJS IncomingMessage
interface CompleteIncomingMessage extends IncomingMessage {
//assert this from parent
url:string;
method:string;
//expand a bit
action:http_action_t;
full_url:URL;
body_string:string;
is_damaged:boolean;
}url,methodthese are optional in originalIncomingMessagebut as we're working with HTTP/HTTPS servers we can assert them in danmaruactionthis is thehttp_action_tthat is assigned to handle the request. This might come handy in somelayereddesignfull_urlas the originalIncomingMessagehaveurlthat is just a string danmaru creates a full_url object from URL class factoring inHostheader and allowing for easy search params (GET params) access wiareq.full_url.searchParams.get('paramname')body_stringin case of request with a body the text is collected here fully before the call tohttp_action_cbis done. If the body goes above limitingmax_body_sizeofhttp_action_toroptions_tdanmaru will return400 BAD REQUESTandhttp_action_cbwill NOT be calledis_damagedis flag set and used internally by danmaru to mark aIncomingMessageas damaged by error, cancellation ormax_body_sizeviolation such request should not reachhttp_action_cb
SimpleServerResponse
The danmaru request object extending from NodeJS ServerResponse
export interface SimpleServerResponse extends ServerResponse {
action:http_action_t;
req_url:string;
logger:logger_t;
auto_headers:OutgoingHttpHeaders;
simple_response:(code:number,data?:any, headers?:OutgoingHttpHeaders, reason?:string)=>boolean;
json_response:(code:number,data:string|object, headers?:OutgoingHttpHeaders, reason?:string)=>boolean;
}actionthis is thehttp_action_tthat is assigned to handle the request. This might come handy in somelayereddesignreq_urla copy of the associatedIncomingMessage.urlkept for easy logging.loggerthe logger from associatedcomposeauto_headersfrom thecomposeoptionssimple_responsemethod for "one line response":resp.simple_response(200)is all it take to "confirm" ahttp_action_cbyou can specify the response body data indata, set additional headers inheadersor customise the response text viareasonif you like.json_responsemethod for "one line JSON response":resp.simple_response(200,{ok:true})is all it take to create return some JSON to the requester. It resolves tosimple_responsewith proper stringification, if needed, plus aContent-Type: application/json; charset=UTF-8header to make the standard committee happy.
#logger_t
the log4js inspired logging facility
interface logger_t {
debug:logfunction_t;
info:logfunction_t;
warn:logfunction_t;
error:logfunction_t;
mark:logfunction_t;
}this provided utility function explains it all:
function log_all():logger_t {
return {
debug:console.log,
info:console.log,
warn:console.log,
error:console.log,
mark:console.log,
}
}For static files serving we recoomend using serve-handler and calling it in indexer function
import {compose} from 'danmaru';
import serveHandler from 'serve-handler'
....
compose(server,[...],{
indexer:(req,resp)=>{
if (req.url.startsWith('/api/')) {
log.info("unhandled api call at "+req.url);
resp.simple_response(codes.NOT_FOUND);
return;
}
serveHandler(req,resp,{public:"./fe/dist", rewrites:[{source:'/',destination:'/index.html'}]});
}
});For more (advanced) examples take a look at our test files :)
