This guide covers important aspects of running SRouter applications in production, including performance optimization, security hardening, and graceful shutdown procedures.
SRouter is designed with performance in mind, building upon the speed of julienschmidt/httprouter. Here are some factors and tips related to performance:
SRouter inherits the high-performance path matching capabilities of julienschmidt/httprouter. This router uses a radix tree structure, allowing for path lookups that are generally O(k), where k is the length of the path, or even O(1) in many practical scenarios, significantly faster than routers relying solely on regular expressions for every route.
Each middleware layer introduces some runtime cost. Limit global and route-specific middleware to what you actually need. For a detailed explanation of how SRouter orders and applies middleware, see Custom Middleware.
SRouter aims to minimize memory allocations in the hot path (request processing). However, certain operations can still lead to allocations:
- Context Management: While
SRouterContextavoids deep nesting, adding values to the context still involves allocations. - Logging: Structured logging (like with
zap) involves allocations for creating log entries and fields. Ensure your logger is configured appropriately for production (e.g., sampling, appropriate level) to minimize performance impact. - Data Encoding/Decoding: Codecs (JSON, Proto, etc.) inherently involve allocations for marshaling and unmarshaling data.
- Custom Middleware: Be mindful of allocations within your own middleware functions.
Tips for Reducing Allocations:
- Use
sync.Poolfor frequently allocated temporary objects within handlers or middleware if profiling shows significant allocation pressure. - Avoid unnecessary string formatting or concatenation within the request path.
- Reuse objects like HTTP clients or database connections instead of creating them per request.
Setting appropriate timeouts via GlobalTimeout, SubRouterConfig.Overrides.Timeout, and route-level Overrides.Timeout is crucial for both performance and stability:
- Prevents slow client connections or long-running handlers from consuming server resources indefinitely.
- Helps protect against certain types of Denial-of-Service (DoS) attacks.
- Ensures predictable response times for clients.
Set timeouts based on the expected latency of the underlying operations for each route or group of routes.
Configuring maximum request body sizes using GlobalMaxBodySize, SubRouterConfig.Overrides.MaxBodySize, and route-level Overrides.MaxBodySize is important for:
- Security: Prevents DoS attacks where clients send excessively large request bodies to exhaust server memory or bandwidth.
- Performance: Avoids processing unnecessarily large amounts of data.
Set limits based on the expected maximum payload size for each endpoint.
Consider running Go's built-in benchmarking tools (go test -bench=.) on your handlers and middleware to identify performance bottlenecks. SRouter itself likely includes benchmarks for its core routing and middleware components.
This section provides security-related guidance for developers using the SRouter framework. While SRouter aims to provide secure defaults and mechanisms, the ultimate security of your application depends on its proper configuration, your specific implementation choices, and adherence to general security best practices.
Correctly extracting the client IP is crucial for logging, rate limiting, and other security features. SRouter provides extensive configuration via IPConfig to handle proxies and custom headers. See the dedicated IP Configuration documentation for details and security recommendations.
SRouter provides a flexible authentication framework through its pkg/middleware/auth.go package. It includes several built-in providers and allows for custom authentication logic.
BearerTokenProvider: Authenticates requests based on a bearer token provided in theAuthorizationheader. It can validate tokens against a predefined map or a custom validator function.APIKeyProvider: Authenticates requests based on an API key that can be provided in a specified HTTP header or a URL query parameter. It validates keys against a predefined map.BasicUserAuthProvider: Implements HTTP Basic Authentication. It extracts username and password from theAuthorizationheader and uses a custom function (GetUserFunc) to validate credentials and retrieve a user object.BearerTokenUserAuthProvider: Similar toBearerTokenProvider, but uses a custom function (GetUserFunc) to validate the token and retrieve a user object.APIKeyUserAuthProvider: Similar toAPIKeyProvider, but uses a custom function (GetUserFunc) to validate the API key and retrieve a user object.
The framework also provides generic middleware functions like AuthenticationWithProvider, Authentication, AuthenticationBool, AuthenticationWithUserProvider, and AuthenticationWithUser that can be used with these providers or with custom authentication functions.
When implementing authentication, especially with custom validator functions (Validator in BearerTokenProvider, GetUserFunc in various UserAuthProvider implementations), it is crucial to follow security best practices:
- Secure Validation Logic: Users are responsible for implementing secure token and credential validation logic within their custom functions.
- Timing Attacks: If you are comparing secrets, tokens, or API keys directly in your custom validation functions, it is highly recommended to use constant-time comparison functions. Standard string or byte slice comparisons (e.g.,
==operator orbytes.Compare) can be vulnerable to timing attacks, where an attacker can infer the value of a secret by measuring the time it takes for the comparison to complete. Go'scrypto/subtlepackage provides functions likesubtle.ConstantTimeComparefor this purpose. - Using cryptographically strong random tokens (e.g., UUIDs, or tokens generated by a secure random number generator) as bearer tokens or API keys can mitigate the need for constant-time comparison if the tokens are sufficiently long and unique, as the primary check becomes existence in a database or a secure store, rather than direct comparison against a static secret.
- Timing Attacks: If you are comparing secrets, tokens, or API keys directly in your custom validation functions, it is highly recommended to use constant-time comparison functions. Standard string or byte slice comparisons (e.g.,
- Secure Storage and Management:
- API keys, tokens, and any other secrets should be stored securely. Avoid hardcoding them in source code. Use environment variables, configuration files with restricted access, or dedicated secret management systems (e.g., HashiCorp Vault, AWS Secrets Manager, Google Cloud Secret Manager).
- If storing password hashes, use a strong, adaptive hashing algorithm like bcrypt, scrypt, or Argon2.
- Implement proper key rotation policies for API keys and other long-lived credentials.
- HTTPS Everywhere: Always use HTTPS (TLS) to protect credentials and session tokens while they are in transit between the client and the server. Transmitting credentials over plain HTTP can expose them to interception.
- Logging: The authentication middleware functions in SRouter may accept a logger instance. While the default logging for authentication failures includes non-sensitive information like method, path, and remote address, be cautious if you customize logging within your authentication handlers or providers. Avoid logging sensitive information such as raw tokens, passwords, or full authorization headers unless they are appropriately masked or redacted.
- Error Messages: The framework uses generic "Unauthorized" messages for authentication failures, which is good practice as it prevents leaking information about whether a username exists or if a password was incorrect. Maintain this practice in any custom error handling.
- Rate Limiting and Account Lockout: Consider implementing rate limiting on authentication attempts and account lockout mechanisms to protect against brute-force attacks. (SRouter may provide separate middleware for rate limiting, which should be used in conjunction with authentication).
- Principle of Least Privilege: Ensure that authenticated users or API keys only have the permissions necessary to perform their intended actions.
SRouter includes a rate limiting middleware (pkg/middleware/ratelimit.go) to help protect your application from abuse, brute-force attacks, and Denial of Service (DoS) by limiting the number of requests a client can make to your API endpoints within a given time window.
- Prevent Abuse: Stop malicious actors from overwhelming your service with requests.
- Ensure Fair Usage: Provide equitable access to resources for all users.
- Prevent DoS/DDoS: Mitigate the impact of denial-of-service attacks by limiting the request rate from individual sources.
- Cost Control: For services that incur costs per request, rate limiting can help manage expenses.
The rate limiting middleware in SRouter supports several strategies for identifying clients:
StrategyIP: Limits requests based on the client's IP address.- Security Note: The effectiveness and security of this strategy heavily depend on the correct configuration of IP address extraction. Please refer to the "IP Address Configuration" section of this document. If
TrustProxyis incorrectly set totruein an environment where proxy headers can be spoofed, attackers might bypass IP-based rate limits by forging headers likeX-Forwarded-For.
- Security Note: The effectiveness and security of this strategy heavily depend on the correct configuration of IP address extraction. Please refer to the "IP Address Configuration" section of this document. If
StrategyUser: Limits requests based on the authenticated user ID.- Security Note: This strategy relies on the authentication middleware (see the "Authentication" section) to correctly and securely identify and authenticate users. If the authentication mechanism is weak or bypassed, user-based rate limiting will not be effective.
StrategyCustom: Allows for a user-definedKeyExtractorfunction to determine the unique key for rate limiting.- Security Note: The security and effectiveness of this strategy depend entirely on the implementation of the custom
KeyExtractorfunction. Ensure that the extracted key accurately represents the entity you wish to rate limit and cannot be easily manipulated by attackers to bypass limits or affect other users.
- Security Note: The security and effectiveness of this strategy depend entirely on the implementation of the custom
SRouter's default rate limiter, UberRateLimiter (which uses go.uber.org/ratelimit), stores rate limiting state in memory for each unique key (e.g., each IP address or user ID).
- High Cardinality Warning: If your application anticipates a very large number of unique keys (e.g., millions of different IP addresses making requests over a short period, or a vast number of user accounts being actively rate-limited simultaneously) without application restarts, this in-memory storage can lead to significant memory consumption.
- Suitability: The current
UberRateLimiteris well-suited for many common use cases, especially for applications with a moderate number of active users or for services deployed behind a load balancer that already handles a significant portion of traffic. - Alternatives for Extreme Scale: For extremely high-traffic scenarios with very high cardinality of rate limiting keys, or if you require persistence of rate limiting state across application restarts or a distributed environment, consider:
- Using an external rate-limiting solution (e.g., Redis-based, or dedicated proxy/API gateway rate limiters).
- Implementing a custom
RateLimiterfor SRouter that uses a backend store with eviction policies (e.g., LRU cache, Redis with TTLs) to manage memory usage.
When a request is subject to rate limiting, SRouter's middleware will typically add the following standard HTTP headers to the response:
X-RateLimit-Limit: The maximum number of requests allowed in the current time window.X-RateLimit-Remaining: The number of requests remaining in the current time window.X-RateLimit-Reset: The time (in UTC seconds since epoch) when the rate limit will reset.Retry-After: If the limit is exceeded, this header may be included, indicating how many seconds the client should wait before making another request. The value will correspond to when the current window resets.
These headers allow clients to understand the rate limits and adjust their request patterns accordingly.
While SRouter provides robust mechanisms for request routing, middleware execution, encoding/decoding of request/response bodies (e.g., JSON), and setting limits like maximum request body size, the detailed validation of input content and the sanitization of data to prevent attacks like Cross-Site Scripting (XSS) are primarily the developer's responsibility within their route handlers.
SRouter itself does not perform deep inspection or validation of the data fields within request bodies or parameters beyond basic parsing and type conversion where applicable.
- Body Size Limits: The
RouteConfigallows setting aMaxBodyBytesto limit the size of incoming request bodies, which can help prevent certain types of denial-of-service attacks caused by excessively large payloads. - Generic Route Sanitizer: For routes registered using
router.RegisterGenericRoute, theRouteConfigaccepts an optionalSanitizerfunction with the signaturefunc(T) (T, error).- This function is executed before the main handler and after the request body has been read (up to
MaxBodyBytes). - It can be used to perform both sanitization (modifying the input to remove malicious parts) and validation (checking if the input conforms to expected formats, ranges, or patterns).
- If the
Sanitizerfunction returns an error, the request is typically rejected by the framework with an appropriate HTTP error code (e.g.,400 Bad Request), and the main handler is not called. - Refer to the example at
examples/generic/main.gofor a demonstration of how to implement and use aSanitizerfunction.
- This function is executed before the main handler and after the request body has been read (up to
It is strongly recommended that developers implement robust input validation and sanitization for all incoming data sources within their application logic:
- Request Bodies:
- Validate the structure and data types of JSON/XML payloads.
- Check for required fields, string lengths, numerical ranges, enumeration values, and specific formats (e.g., email addresses, UUIDs).
- Query Parameters:
- Validate the type and format of each query parameter.
- Be mindful of parameters used in database queries or other sensitive operations.
- Path Parameters:
- While the underlying
httprouter(or a similar routing library) handles the basic matching of URL paths, the actual content of a path parameter might still require validation. For example, if a path parameter is expected to be a numeric ID, ensure it is indeed a number and within an acceptable range.
- While the underlying
- HTTP Headers:
- Validate any custom or standard HTTP headers that your application relies on for its logic.
- Consider using popular Go libraries for validation to simplify and standardize your validation logic. For instance,
go-playground/validatoris widely used for struct-based validation using tags, allowing you to define validation rules directly in your data structures.
While SRouter itself may not directly cause or prevent these, developers should be mindful of common web application vulnerabilities related to input/output handling:
- Cross-Site Scripting (XSS):
- If your application generates HTML content dynamically based on user input, ensure that all user-supplied data is properly sanitized or escaped before being rendered.
- Using Go's
html/templatepackage provides context-aware auto-escaping, which is highly effective against XSS when used correctly. - Set appropriate
Content-Security-Policy(CSP) headers to further restrict the capabilities of scripts in the browser.
- SQL Injection (SQLi):
- If your application interacts with SQL databases, always use parameterized queries or prepared statements.
- Avoid constructing SQL queries by concatenating user-supplied strings directly.
- Use an ORM (like GORM, used in some SRouter examples) that handles SQL parameterization, but still be mindful of how you construct queries with user input even when using an ORM.
- Command Injection: If user input is used to construct shell commands or arguments, ensure it is properly sanitized to prevent attackers from executing arbitrary commands on the server.
- Directory Traversal: If user input is used to construct file paths, validate and sanitize it to prevent access to unintended files or directories.
By diligently validating and sanitizing all inputs, and by being aware of common output encoding needs, developers can significantly improve the security posture of their SRouter-based applications.
Cross-Origin Resource Sharing (CORS) is a browser security feature that restricts web pages from making requests to a different domain (origin) than the one that served the web page. This is a crucial security measure to prevent malicious websites from reading sensitive data from other websites or performing unauthorized actions on behalf of the user.
SRouter provides a built-in CORS middleware that can be configured through the CORSConfig field in the main RouterConfig. This middleware automatically handles preflight OPTIONS requests and adds the necessary CORS headers to responses.
The CORSConfig struct within RouterConfig offers the following options to control CORS behavior:
Origins([]string): A list of allowed origins. An origin is a combination of scheme (e.g.,http,https), domain, and port.- Example:
[]string{"https://www.example.com", "https://api.example.com"} - A wildcard
"*"can be used to allow all origins. However, see the security implications below.
- Example:
Methods([]string): A list of allowed HTTP methods for cross-origin requests (e.g.,"GET","POST","PUT","DELETE").Headers([]string): A list of allowed HTTP headers that the client can send in a cross-origin request.AllowCredentials(bool): If set totrue, the browser will allow cookies and other credentials (like HTTP Basic authentication) to be sent with cross-origin requests.- Security Note: This requires careful consideration and alignment with
Originssettings.
- Security Note: This requires careful consideration and alignment with
MaxAge(int): Specifies how long the results of a preflight request (anOPTIONSrequest) can be cached by the browser, in seconds.ExposeHeaders([]string): A list of response headers that the browser should allow client-side JavaScript to access. By default, many headers are not exposed.
-
Originsand Wildcards ("*"):- Using a wildcard
"*"forOriginsis the most permissive setting, allowing any domain to make requests to your API. While this is flexible, it may not always be the most secure choice. - Recommendation: For better security, explicitly list the origins that are permitted to access your resources. This is especially important for applications handling sensitive data.
- Important: If
AllowCredentialsis set totrue, theAccess-Control-Allow-Originheader cannot be a wildcard ("*"). SRouter's CORS middleware correctly handles this by ensuring that ifAllowCredentialsis true andOriginscontains*, it will dynamically set theAccess-Control-Allow-Originto the specific origin of the request if that origin is found within the configuredOriginslist (excluding the wildcard itself for this specific header when credentials are allowed). If no specific match is found, the request may be rejected or the credential-related headers might not be set appropriately.
- Using a wildcard
-
AllowCredentials(bool):- Setting
AllowCredentialstotruemeans that the server is willing to process cookies or other user credentials sent by the browser with cross-origin requests. - Security Warning: Only enable this if your application specifically requires credentialed cross-origin requests (e.g., for session management with a separate frontend domain).
- When
AllowCredentialsistrue, theAccess-Control-Allow-Originheader must be a specific origin, not a wildcard. Ensure yourOriginslist is configured accordingly.
- Setting
-
ExposeHeaders([]string):- By default, browsers restrict access to many response headers for client-side JavaScript. If your frontend needs to read specific headers from cross-origin responses (e.g., a custom pagination header or a rate limit header), you must include them in the
ExposeHeaderslist. - Only expose headers that are strictly necessary for the client application to function.
- By default, browsers restrict access to many response headers for client-side JavaScript. If your frontend needs to read specific headers from cross-origin responses (e.g., a custom pagination header or a rate limit header), you must include them in the
-
Handling of Preflight
OPTIONSRequests: SRouter's CORS middleware automatically handles preflightOPTIONSrequests. These requests are sent by the browser before the actual request (e.g.,GET,POST) to determine if the server permits the cross-origin communication with the specified method and headers. -
Restrictive Policies:
- Recommendation: Always configure your CORS policy to be as restrictive as possible while still allowing your legitimate frontend applications to function correctly.
- Avoid using wildcards for
Origins,Methods, orHeadersunless absolutely necessary and the security implications are fully understood. - Regularly review your CORS configuration to ensure it aligns with your application's current needs and security posture.
By carefully configuring these options, you can ensure that your SRouter application interacts securely with web browsers from different origins.
Building a secure application is a multifaceted endeavor that goes beyond the scope of a single framework. While SRouter provides tools to help you build secure web applications, remember the following general principles:
- Keep Dependencies Up-to-Date: Regularly update SRouter and all other third-party libraries to their latest stable versions. Security vulnerabilities are often discovered and patched in newer releases. Use tools like
go list -u -m allandgo getto manage your Go module dependencies. - Secure Deployment Environment: Ensure your deployment environment (servers, containers, operating systems, network configurations) is hardened and configured securely.
- Handle Sensitive Data with Care:
- Minimize the amount of sensitive data you collect and store.
- Encrypt sensitive data at rest and in transit (HTTPS).
- Implement appropriate access controls for sensitive information.
- Be mindful of logging sensitive data; redact or avoid logging it whenever possible.
- Regular Security Testing:
- Conduct regular security audits and penetration testing of your application.
- Utilize static analysis security testing (SAST) and dynamic analysis security testing (DAST) tools.
- Consider implementing a bug bounty program if appropriate for your application.
- Follow Secure Coding Practices: Adhere to general secure coding principles, such as the OWASP Top Ten, to avoid common web application vulnerabilities.
- Defense in Depth: Employ multiple layers of security controls. No single security measure is foolproof.
- Stay Informed: Keep abreast of the latest security threats and best practices in web application development.
By combining the secure features of SRouter with these general best practices, you can significantly enhance the security posture of your applications.
Properly shutting down your HTTP server is crucial to avoid abruptly terminating active connections and potentially losing data or leaving operations in an inconsistent state. SRouter provides mechanisms to support graceful shutdown.
The SRouter instance (*router.Router) itself has a Shutdown method. Calling this marks the router as shutting down so no new requests are served, waits for any in‑flight requests to finish, and stops internal components such as the trace ID generator (if enabled).
// func (r *Router[T, U]) Shutdown(ctx context.Context) errorIt takes a context.Context which can be used to set a deadline for the shutdown process.
The standard Go net/http package provides the http.Server.Shutdown method for gracefully shutting down the server. This method stops the server from accepting new connections and waits for active requests to complete before returning.
You should call both the http.Server.Shutdown and the router.Router.Shutdown methods when handling termination signals (like SIGINT or SIGTERM).
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall" // Use syscall for SIGTERM if needed
"time"
"github.com/Suhaibinator/SRouter/pkg/router" // Assuming router setup as 'r'
"go.uber.org/zap" // Assuming logger setup as 'logger'
)
func main() {
// --- Assume router 'r' is configured and created here ---
logger, _ := zap.NewProduction() // Example logger
defer logger.Sync()
// ... router config ...
r := router.NewRouter[string, string](/* ... config, auth funcs ... */)
// ... register routes ...
// Create the HTTP server
srv := &http.Server{
Addr: ":8080",
Handler: r, // Use the SRouter instance as the handler
// Add other server configurations like ReadTimeout, WriteTimeout if desired
}
// Start the server in a separate goroutine so it doesn't block
go func() {
log.Println("Server starting on", srv.Addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("ListenAndServe failed: %v", err)
}
}()
// Wait for interrupt signal (SIGINT) or termination signal (SIGTERM)
quit := make(chan os.Signal, 1)
// signal.Notify(quit, os.Interrupt) // Catch only Ctrl+C
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // Catch Ctrl+C and SIGTERM
receivedSignal := <-quit
log.Printf("Received signal: %s. Shutting down server...\n", receivedSignal)
// Create a context with a timeout for the shutdown process
// Give active requests time to finish (e.g., 5 seconds)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// --- Perform Shutdown ---
// 1. Shut down the SRouter instance (stops new requests and halts internal components like the trace ID generator)
// It's generally good practice to shut down the router logic first.
if err := r.Shutdown(ctx); err != nil {
log.Printf("SRouter shutdown failed: %v\n", err)
// Decide if this error is fatal or if you should proceed
} else {
log.Println("SRouter shut down successfully.")
}
// 2. Shut down the HTTP server (stops accepting new connections, waits for active requests)
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("HTTP server shutdown failed: %v", err) // Often fatal if server can't shut down
} else {
log.Println("HTTP server shut down successfully.")
}
log.Println("Server exited gracefully")
}- Receive Signal: Detect
SIGINTorSIGTERM. - Create Context: Create a
context.WithTimeoutto limit the shutdown duration. - Router Shutdown: Call
r.Shutdown(ctx)to signal SRouter's internal components. - Server Shutdown: Call
srv.Shutdown(ctx)to stop accepting new connections and wait for existing requests handled by SRouter to complete.
This ensures that the server stops accepting new work, SRouter components are notified, and active requests are given time to finish before the application exits.
See the examples/graceful-shutdown directory for a runnable example.