Show / Hide Table of Contents

    Middleware System

    TMS Sparkle provides classes that allow you to create middleware interfaces and add them to the request processing pipeline. In other words, you can add custom functionality that pre-process the request before it's effectively processed by the main server module, and post-process the response provided by it.

    Using middleware interfaces allows you to easily extend existing server modules without changing them. It also makes the request processing more modular and reusable. For example, the compression middleware is a generic one that compress the server response if the client accepts it. It can be added to any server module (XData, RemoteDB, Echo, or any other module you might create) to add such functionality.

    A middleware is represented by the interface IHttpServerMiddleware, declared in unit Sparkle.HttpServer.Module. To add a middleware interface to a server module, just use the AddMiddleware function (in this example, we're using a TMS XData module, but can be any Sparkle server module):

    var
      MyMiddleware: IHttpServerMiddleware;
      Module: TXDataServerModule;
    {...}
      // After retrieving the MyMiddleware interface, add it to the dispatcher
      Module.AddMiddleware(MyMiddleware);
      Dispatcher.AddModule(Module);
    

    The following topics explain in deeper details how to use middlewares with TMS Sparkle.

    Compress Middleware

    Use the Compress middleware to allow the server to return the response body in compressed format (gzip or deflate). The compression will happen only if the client sends the request with the header "accept-encoding" including either gzip or deflate encoding. If it does, the server will provide the response body compressed with the requested encoding. If both are accepted, gzip is preferred over deflate.

    To use the middleware, just create an instance of TCompressMiddleware (declared in Sparkle.Middleware.Compress unit) and add it to the server module:

    uses {...}, Sparkle.Middleware.Compress;
    {...}
      Module.AddMiddleware(TCompressMiddleware.Create);
    

    Setting a threshold

    TCompressMiddleware provides the Threshold property which allows you to define the minimum size for the response body to be compressed. If it's smaller than that size, no compression will happen, regardless of the 'accept-encoding' request header value. The default value is 1024, but you can change it:

    var
      Compress: ICompressMiddleware;
    begin
      Compress := TCompressMiddleware.Create;
      Compress.Threshold := 2048; // set the threshold as 2k
      Module.AddMiddleware(Compress);
    

    CORS Middleware

    Use the CORS middleware to add CORS support to the Sparkle module. That will allow web browsers to access your server even if it is in a different domain than the web page.

    To use the middleware, just create an instance of TCorsMiddleware (declared in Sparkle.Middleware.Cors unit) and add it to the server module:

    uses {...}, Sparkle.Middleware.Cors;
    {...}
      Module.AddMiddleware(TCorsMiddleware.Create);
    

    Basically the above code will add the Access-Control-Allow-Origin header to the server response allowing any origin:

    Access-Control-Allow-Origin: *
    

    Additional settings

    You can use overloaded Create constructors with additional parameters. First, you can set a different origin for the Access-Control-Allow-Origin:

    Module.AddMiddleware(TCorsMiddeware.Create('somedomain.com'));
    

    You can also configure the HTTP methods allowed by the server, which will be replied in the Access-Control-Allow-Methods header:

    Module.AddMiddleware(TCorsMiddeware.Create('somedomain.com', 'GET,POST,DELETE,PUT'));
    

    And finally the third parameter will specify the value of Access-Control-Max-Age header:

    Module.AddMiddleware(TCorsMiddeware.Create('somedomain.com', 'GET,POST,DELETE,PUT', 1728000));
    

    To illustrate, the above line will add the following headers in the response:

    Access-Control-Allow-Origin: somedomain.com
    Access-Control-Allow-Methods: GET,POST,DELETE,PUT
    Access-Control-Max-Age: 1728000
    

    JWT Authentication Middleware

    Add the JWT Authentication Middleware to implement authentication using JSON Web Token. This middleware will process the authorization header, check if there is a JSON Web Token in it, and if it is, create the user identity and claims based on the content of JWT.

    The middleware class is TJwtMiddleware, declared in unit Sparkle.Middleware.Jwt. It's only available for Delphi XE6 and up.

    To use the middleware, create it passing the secret to sign the JWT, and then add it to the TXDataServerModule object:

    uses {...}, Sparkle.Middleware.Jwt;
    
      Module.AddMiddleware(TJwtMiddleware.Create('my jwt secret'));
    

    By default, the middleware rejects expired tokens and allows anonymous access. You can change this behavior by using the Create constructor as explained below in the Methods table.

    Properties

    Name Description
    Secret: TBytes The secret used to verify the JWT signature. Usually you won't use this property as the secret is passed in the Create constructor.

    Methods

    Name Description
    constructor Create(const ASecret: string; AForbidAnonymousAccess: Boolean = False; AAllowExpiredToken: Boolean = False); Creates the middleware using the secret specified in the ASecret parameter, as string. The string will be converted to bytes using UTF-8 encoding.
    The second boolean parameter is AForbidAnonymousAccess, which is False by default. That means the middleware will allow requests without a token. Pass True to the second parameter to not allow anonymous access.
    The third boolean parameter is AAllowExpiredToken which is False by default. That means expired tokens will be rejected by the server. Pass True to the third parameter to allow rejected tokens.
    constructor Create(const ASecret: TBytes; AForbidAnonymousAccess: Boolean = False; AAllowExpiredToken: Boolean = False); Same as above, but creates the middleware using the secret specified in raw bytes instead of string.

    Basic Authentication Middleware

    Add the Basic Authentication middleware to implement authentication using Basic authentication. This middleware will check the authorization header of the request for user name and password provided using Basic authentication.

    The user name and password will then be passed to a callback method that you need to implement to return an user identity and its claims. Different from the JWT Authentication Middleware which automatically creates the identity based on JWT content, in the basic authentication it's up to you to do that, based on user credentials.

    If your module implementation returns a status code 401, the middleware will automatically add a www-authenticate header to the response informing the client that the request needs to be authenticated, using the specified realm.

    Example:

    uses {...}, Sparkle.Middleware.BasicAuth, Sparkle.Security;
    
      Module.AddMiddleware(TBasicAuthMiddleware.Create(
        procedure(const UserName, Password: string; var User: IUserIdentity)
        begin
          // Implement custom logic to authenticate user based on UserName and Password
          // For example, go to the database, check credentials and then create an user
          // identity with all permissions (claims) for this user
          User := TUserIdentity.Create;
          User.Claims.AddOrSet('roles').AsString := SomeUserPermission;
          User.Claims.AddOrSet('sub').AsString := UserName;
        end,
        'My Server Realm'
      ));
    

    Related types

    TAuthenticateBasicProc = reference to procedure(const UserName, Password: string; var User: IUserIdentity);
    

    The procedure that will be called for authentication. UserName and Password are the user credentials sent by the client, and you must then create and return the IUserIdentity interface in the User parameter.

    Properties

    Name Description
    OnAuthenticate: TAuthenticateBasicProc The authentication callback that will be called when the middleware retrieves the user credentials.
    Realm: string The realm that will be informed in the www-authenticate response header. Default realm is "TMS Sparkle Server".

    Methods

    Name Description
    constructor Create(AAuthenticateProc: TAuthenticateBasicProc; const ARealm: string) Creates the middleware using the specified callback and realm value.
    constructor Create(AAuthenticateProc: TAuthenticateBasicProc) Creates the middleware using the specified callback procedure.

    Logging Middleware

    Add the Logging Middleware to implement log information about requests and responses processed by the Sparkle server. This middleware uses the built-in Logging mechanism to log information about the requests and responses. You must then properly configure the output handlers to specify where data should be logged to.

    The middleware class is TLoggingMiddleware, declared in unit Sparkle.Middleware.Logging.

    To use the middleware, create it, set the desired properties if needed, then add it to the TXDataServerModule object:

    uses {...}, Sparkle.Middleware.Logging;
    
    var
      LoggingMiddleware: TLoggingMiddleware;
    
    begin
      LoggingMiddleware := TLoggingMiddleware.Create;
      // Set LoggingMiddleware properties.
      Module.AddMiddleware(LoggingMiddleware);
    end;
    

    You don't need to set any property for the middleware to work. It will output information using the default format.

    Properties

    Name Description
    FormatString: string Specifies the format string for the message to be logged for each request/response, according to the format string options, described at the end of this topic. The default format string is:
    :method :url :statuscode - :responsetime ms
    ExceptionFormatString: string Specifies the format string for the message to be logged when an exception happens during the request processing. In other words, if any exception occurs in the server during request processing, and LogExceptions property is True, the exception will be logged using this format. Different from FormatString property which has a specific format, the content of this property is just a simple string used by Delphi Format function.
    Three parameters are passed to the Format function, in this order: The exception message, the exception class name and the log message itself. Default value is:
    (%1:s) %0:s - %2:s
    LogExceptions: Boolean Indicates if the middleware should catch unhandled exceptions raised by the server and log them. Default is True. Note if LogExceptions is True logged exceptions will not be propagated up in the chain. If LogExceptions is False, the exception will be raised again to be handled by some other middleware or the low level exception handling system in Sparkle.
    LogLevel: TLogLevel The level of log messages generated by the middleware. It could be Trace, Debug, Info, Warning, Error. Default value is Trace.
    ExceptionLogLevel: TLogLevel The level of exception log messages generated by the middleware. It could be Trace, Debug, Info, Warning, Error. Default value is Error.
    property OnFilterLog: TLoggingFilterProc OnFilterLog event is fired for every incoming request that should be logged. This is an opportunity for you to filter which requests should be logged. For example, you might want to only log messages for requests that return an HTTP status code indicating an error. Below is an example of use.
    property OnFilterLogEx: TLoggingFilterExProc OnFilterExLog event is fired for every incoming request that should be logged. This is an opportunity for you to filter which requests should be logged. For example, you might want to only log messages for requests that return an HTTP status code indicating an error. Below is an example of use.

    TLoggingFilterProc

    TLoggingFilterProc = reference to procedure(Context: THttpServerContext; var Accept: Boolean);
    
    LoggingMiddleware.OnFilterLog := procedure(Context: THttpServerContext; var Accept: Boolean)
      begin
        Accept := Context.Response.StatusCode >= 400;
      end;
    

    TLoggingFilterExProc

    TLoggingFilterExProc = reference to procedure(Context: THttpServerContext;  Info: TLoggingInfo; var Accept: Boolean);
    
    LoggingMiddleware.OnFilterLog := procedure(Context: THttpServerContext; 
      Info: TLoggingInfo; var Accept: Boolean)
      begin
        if Context.Response.StatusCode >= 400 then
        begin
          // log the messages ourselves, with different level 
          Accept := False;
          Info.Logger.Warning(Info.LogMessage);
        end;
      end;
    

    Format string options

    The format string is a string that represents a single log line and utilize a token syntax. Tokens are references by :token-name. If tokens accept arguments, they can be passed using [], for example: :token-name[param] would pass the string 'param' as an argument to the token token-name.

    Each token in the format string will be replaced by the actual content. As an example, the default format string for the logging middleware is

    :method :url :statuscode - :responsetime ms
    

    which means it will output the HTTP method of the request, the request URL, the status code, an hifen, then the response time followed by the letters "ms". This is an example of such output:

    GET /request/path 200 - 4.12 ms
    

    Here is the list of available tokens:

    :method
    The HTTP method of the request, e.g. "GET" or "POST".

    :protocol The HTTP protocol of the request, e.g. "HTTP1/1".

    :req[header]
    The given header of the request. If the header is not present, the value will be displayed as an empty string in the log. Example: :req[content-type] might output "text/plain".

    :reqheaders
    All the request headers in raw format.

    :remoteaddr
    The remote address of the request.

    :res[header]
    The given header of the response. If the header is not present, the value will be displayed as an empty string in the log. Example: :res[content-type] might output "text/plain".

    :resheaders
    All the response headers in raw format.

    :responsetime[digits]
    The time between the request coming into the logging middleware and when the response headers are written, in milliseconds. The digits argument is a number that specifies the number of digits to include on the number, defaulting to 2.

    :statuscode
    The status code of the response.

    :url
    The URL of the request.

    Encryption Middleware

    Add the Encryption Middleware to allow for custom encryption of request and response body message. With this middleware you can add custom functions that process the body content, receiving the input bytes and returning processed output bytes.

    The middleware class is TEncryptionMiddleware, declared in unit Sparkle.Encryption.Logging.

    To use the middleware, create it, set the desired properties if needed, then add it to the TXDataServerModule object:

    uses {...}, Sparkle.Middleware.Encryption;
    
    var
      EncryptionMiddleware: TEncryptionMiddleware;
    
    begin
      EncryptionMiddleware := TEncryptionMiddleware.Create;
      // Set EncryptionMiddleware properties.
      Module.AddMiddleware(EncryptionMiddleware);
    end;
    

    These are the properties you can set. You need to set at the very minimum either EncryptBytes and DecryptBytes properties.

    Properties

    Name Description
    CustomHeader: string If different from empty string, then the request and response will not be performed if the request from the client includes HTTP header with the same name as CustomHeader property and same value as CustomHeaderValue property.
    CustomHeaderValue: string See CustomHeader property for more information.
    EncryptBytes: TEncryptDecryptBytesFunc Set EncryptBytes function to define the encryption processing of the message body. This function is called for every message body that needs to be encrypted. The bytes to be encrypted are passed in the ASource parameter, and the processed bytes should be provided in the ADest parameter.
    If the function returns True, then the encrypted message (content of ADest) will be used. If the function returns False, then the content of ASource will be used (thus the original message will be used).
    DecryptBytes: TEncryptDecryptBytesFunc Set the DecryptBytes function to define the decryption processing of the message body. See property EncryptBytes for more details.

    TEncryptDecryptBytesFunc

    TEncryptDecryptBytesFunc = reference to function(const ASource: TBytes; out ADest: TBytes): Boolean;
    

    Creating Custom Middleware

    To create a new middleware, create a new class descending from THttpServerMiddleware (declared in Sparkle.HttpServer.Module) and override the method ProcessRequest:

    uses {...}, Sparkle.HttpServer.Module;  
    
    type
      TMyMiddleware = class(THttpServerMiddleware, IHttpServerMiddleware)
      protected
        procedure ProcessRequest(Context: THttpServerContext; Next: THttpServerProc); override;
      end;
    

    In the ProcessRequest method you do any processing of the request you want, and then you should call the Next function to pass the request to the next process requestor in the pipeline (until it reaches the main server module processor):

    procedure TMyMiddleware.ProcessRequest(Context: THttpServerContext; Next: THttpServerProc);
    begin
      if Context.Request.Headers.Exists('custom-header') then
        Next(Context)
      else
        Context.Response.StatusCode := 500;
    end;
    

    In the example above, the middleware checks if the header "custom-header" is present in the request. If it does, then it calls the next processor which will continue to process the request normally. If it does not, a 500 status code is returned and processing is done. You can of course modify the request object before forwarding it to the next processor. Then you can use this middleware just by adding it to any server module:

    Module.AddMiddleware(TMyMiddleware.Create);
    

    Alternatively, you can use the TAnonymousMiddleware (unit Sparkle.HttpServer.Module) to quickly create a simple middleware without needing to create a new class. The following example does the same as above:

    Module.AddMiddleware(TAnonymousMiddleware.Create(
    procedure(Context: THttpServerContext; Next: THttpServerProc)
    begin
      if Context.Request.Headers.Exists('custom-header') then
        Next(Context)
      else
        Context.Response.StatusCode := 500;
    end
    ));
    

    Processing the response

    Processing the response requires a different approach because the request must reach the final processor until the response can be post-processed by the middleware. To do that, you need to use the OnHeaders method of the response object. This method is called right after the response headers are build by the final processor, but right before it's sent to the client. So the middleware has the opportunity to get the final response but still modify it before it's sent:

    procedure TMyMiddleware2.ProcessRequest(C: THttpServerContext; Next: THttpServerProc);
    begin
      C.Response.OnHeaders(
        procedure(Resp: THttpServerResponse)
        begin
          if Resp.Headers.Exists('some-header') then
            Resp.StatusCode := 500;
        end
      );
      Next(C);
    end;
    

    The above middleware code means: when the response is about to be sent to the client, check if the response has the header "some-header". If it does, then return with status code of 500. Otherwise, continue normally.

    Generic Middleware

    Generic middleware provides you an opportunity to add code that does any custom processing for the middleware that is not covered by the existing pre-made middleware classes.

    The middleware class is TSparkleGenericMiddleware, declared in unit Sparkle.Comp.GenericMiddleware. It's actually just a wrapper around the techniques described to create a custom middleware, but available at design-time.

    This middleware publishes two events:

    Events

    Name Description
    OnRequest: TMiddlewareRequestEvent This event is fired to execute the custom code for the middleware. Please refer to Creating Custom Middleware to know how to implement such code. If you don't call at least Next(Context) in the code, your server will not work as the request chain will not be forwarded to your module. Below is an example.
    OnMiddlewareCreate: TMiddlewareCreateEvent Fired when the IHttpServerMiddleware interface is created. You can use this event to actually create your own IHttpServerMiddleware interface and pass it in the Middleware parameter to be used by the Sparkle module.

    TMiddlewareRequestEvent

    TMiddlewareRequestEvent = procedure(Sender: TObject; Context: THttpServerContext; Next: THttpServerProc) of object;
    
    procedure TForm3.XDataServer1GenericRequest(Sender: TObject;
      Context: THttpServerContext; Next: THttpServerProc);
    begin
      if Context.Request.Headers.Exists('custom-header') then
        Next(Context)
      else
        Context.Response.StatusCode := 500;
    end;
    

    TMiddlewareCreateEvent

    TMiddlewareCreateEvent = procedure(Sender: TObject; var Middleware: IHttpServerMiddleware) of object;
    

    Passing data through context

    In many situations you would want do add additional meta informatino to the request context, to be used by further middleware and module.

    For example, you might create a database connection and let it be used by other middleware, or you want to set a specific id, etc. You can do that by using Data and Item properties of the context:

    procedure TForm3.XDataServerFirstMiddlewareRequest(Sender: TObject; Context: THttpServerContext; Next: THttpServerProc);
    var
      Conn: IDBConnection;
      Obj: TMyObject;
    begin
      // Set an ID, object and specific interface to the context
      Obj := TMyObject.Create;
      try
        Conn := GetSomeConnection;
    
        Context.Data['TenantId'] := 5;
        Context.SetItem(Obj);
        Context.SetItem(Conn);
        Next(Context);
      finally
        Obj.Free;
      end;
    end;
    

    Then you can use it in further middleware and/or module this way:

    procedure TForm3.XDataServerSecondMiddlewareRequest(Sender: TObject; Context: THttpServerContext; Next: THttpServerProc);
    var
      Conn: IDBConnection;
      Obj: TMyObject;
      TenantId: Integer;
    begin
      Conn := Context.Item<IDBConnection>;
      if Conn <> nil then ; // do something with connection
    
      Obj := Context.Item<TMyObject>;
      if Obj <> nil then ; // do something with Obj
    
      if not Context.Data['TenantId'].IsEmpty then
        // use TenantId
        TenantId := Context.Data['TenantId'].AsInteger;
    
      Next(Context);
    end;
    

    Finally, you can use such context data from anywhere in your application, using the global reference to the request. The global reference uses a thread-var, so you must access it from the same thread which is processing the request:

    procedure TMyForm.SomeMethod;
    var
      Context: THttpServerContext;
      Conn: IDBConnection;
      Obj: TMyObject;
      TenantId: Integer;
    begin
      Context := THttpServerContext.Current;
    
      // now get custom data from context
      Conn := Context.Item<IDBConnection>;
      if Conn <> nil then ; // do something with connection
    
      Obj := Context.Item<TMyObject>;
      if Obj <> nil then ; // do something with Obj
    
      if not Context.Data['TenantId'].IsEmpty then
        // use TenantId
        TenantId := Context.Data['TenantId'].AsInteger;
    end;
    
    Back to top TMS Sparkle v3.15
    © 2002 - 2021 tmssoftware.com