in router/core/router.go [245:597]
func NewRouter(opts ...Option) (*Router, error) {
r := &Router{
EngineStats: statistics.NewNoopEngineStats(),
}
for _, opt := range opts {
opt(r)
}
if r.logger == nil {
r.logger = zap.NewNop()
}
// Default value for graphql path
if r.graphqlPath == "" {
r.graphqlPath = "/graphql"
}
if r.graphqlWebURL == "" {
r.graphqlWebURL = r.graphqlPath
}
// this is set via the deprecated method
if !r.playground {
r.playgroundConfig.Enabled = r.playground
r.logger.Warn("The playground_enabled option is deprecated. Use the playground.enabled option in the config instead.")
}
if r.playgroundPath != "" && r.playgroundPath != "/" {
r.playgroundConfig.Path = r.playgroundPath
r.logger.Warn("The playground_path option is deprecated. Use the playground.path option in the config instead.")
}
if r.playgroundConfig.Path == "" {
r.playgroundConfig.Path = "/"
}
if r.instanceID == "" {
r.instanceID = nuid.Next()
}
r.processStartTime = time.Now()
// Create noop tracer and meter to avoid nil pointer panics and to avoid checking for nil everywhere
r.tracerProvider = sdktrace.NewTracerProvider(sdktrace.WithSampler(sdktrace.NeverSample()))
r.otlpMeterProvider = sdkmetric.NewMeterProvider()
r.promMeterProvider = sdkmetric.NewMeterProvider()
// Default values for trace and metric config
if r.traceConfig == nil {
r.traceConfig = rtrace.DefaultConfig(Version)
}
if r.metricConfig == nil {
r.metricConfig = rmetric.DefaultConfig(Version)
}
if r.corsOptions == nil {
r.corsOptions = CorsDefaultOptions()
}
if r.subgraphTransportOptions == nil {
r.subgraphTransportOptions = DefaultSubgraphTransportOptions()
}
if r.graphqlMetricsConfig == nil {
r.graphqlMetricsConfig = DefaultGraphQLMetricsConfig()
}
if r.routerTrafficConfig == nil {
r.routerTrafficConfig = DefaultRouterTrafficConfig()
}
if r.fileUploadConfig == nil {
r.fileUploadConfig = DefaultFileUploadConfig()
}
if r.accessController != nil {
if len(r.accessController.authenticators) == 0 && r.accessController.authenticationRequired {
r.logger.Warn("authentication is required but no authenticators are configured")
}
}
if r.ipAnonymization == nil {
r.ipAnonymization = &IPAnonymizationConfig{
Enabled: true,
Method: Redact,
}
}
// Default values for health check paths
if r.healthCheckPath == "" {
r.healthCheckPath = "/health"
}
if r.readinessCheckPath == "" {
r.readinessCheckPath = "/health/ready"
}
if r.livenessCheckPath == "" {
r.livenessCheckPath = "/health/live"
}
r.headerRules = AddCacheControlPolicyToRules(r.headerRules, r.cacheControlPolicy)
hr, err := NewHeaderPropagation(r.headerRules)
if err != nil {
return nil, err
}
if hr.HasRequestRules() {
r.preOriginHandlers = append(r.preOriginHandlers, hr.OnOriginRequest)
}
if hr.HasResponseRules() {
r.postOriginHandlers = append(r.postOriginHandlers, hr.OnOriginResponse)
}
defaultHeaders := []string{
// Common headers
"authorization",
"origin",
"content-length",
"content-type",
// Semi standard client info headers
"graphql-client-name",
"graphql-client-version",
// Apollo client info headers
"apollographql-client-name",
"apollographql-client-version",
// Required for WunderGraph ART
"x-wg-trace",
"x-wg-disable-tracing",
"x-wg-token",
"x-wg-skip-loader",
"x-wg-include-query-plan",
// Required for Trace Context propagation
"traceparent",
"tracestate",
// Required for feature flags
"x-feature-flag",
}
if r.clientHeader.Name != "" {
defaultHeaders = append(defaultHeaders, r.clientHeader.Name)
}
if r.clientHeader.Version != "" {
defaultHeaders = append(defaultHeaders, r.clientHeader.Version)
}
defaultMethods := []string{
"HEAD", "GET", "POST",
}
r.corsOptions.AllowHeaders = stringsx.RemoveDuplicates(append(r.corsOptions.AllowHeaders, defaultHeaders...))
r.corsOptions.AllowMethods = stringsx.RemoveDuplicates(append(r.corsOptions.AllowMethods, defaultMethods...))
if r.tlsConfig != nil && r.tlsConfig.Enabled {
r.baseURL = fmt.Sprintf("https://%s", r.listenAddr)
} else {
r.baseURL = fmt.Sprintf("http://%s", r.listenAddr)
}
if r.tlsConfig != nil && r.tlsConfig.Enabled {
if r.tlsConfig.CertFile == "" {
return nil, errors.New("tls cert file not provided")
}
if r.tlsConfig.KeyFile == "" {
return nil, errors.New("tls key file not provided")
}
var caCertPool *x509.CertPool
clientAuthMode := tls.NoClientCert
if r.tlsConfig.ClientAuth != nil && r.tlsConfig.ClientAuth.CertFile != "" {
caCert, err := os.ReadFile(r.tlsConfig.ClientAuth.CertFile)
if err != nil {
return nil, fmt.Errorf("failed to read cert file: %w", err)
}
// Create a CA an empty cert pool and add the CA cert to it to serve as authority to validate client certs
caPool := x509.NewCertPool()
if ok := caPool.AppendCertsFromPEM(caCert); !ok {
return nil, errors.New("failed to append cert to pool")
}
caCertPool = caPool
if r.tlsConfig.ClientAuth.Required {
clientAuthMode = tls.RequireAndVerifyClientCert
} else {
clientAuthMode = tls.VerifyClientCertIfGiven
}
r.logger.Debug("Client auth enabled", zap.String("mode", clientAuthMode.String()))
}
// Load the server cert and private key
cer, err := tls.LoadX509KeyPair(r.tlsConfig.CertFile, r.tlsConfig.KeyFile)
if err != nil {
return nil, fmt.Errorf("failed to load tls cert and key: %w", err)
}
r.tlsServerConfig = &tls.Config{
ClientCAs: caCertPool,
Certificates: []tls.Certificate{cer},
ClientAuth: clientAuthMode,
}
}
if r.traceConfig.Enabled {
if len(r.traceConfig.Propagators) > 0 {
propagators, err := rtrace.BuildPropagators(r.traceConfig.Propagators...)
if err != nil {
r.logger.Error("creating propagators", zap.Error(err))
return nil, err
}
r.tracePropagators = propagators
}
// Add default tracing exporter if needed
if len(r.traceConfig.Exporters) == 0 && r.traceConfig.TestMemoryExporter == nil {
if endpoint := otelconfig.DefaultEndpoint(); endpoint != "" {
r.logger.Debug("Using default trace exporter", zap.String("endpoint", endpoint))
r.traceConfig.Exporters = append(r.traceConfig.Exporters, &rtrace.ExporterConfig{
Endpoint: endpoint,
Exporter: otelconfig.ExporterOLTPHTTP,
HTTPPath: "/v1/traces",
Headers: otelconfig.DefaultEndpointHeaders(r.graphApiToken),
})
}
}
}
// Add default metric exporter if none are configured
if r.metricConfig.OpenTelemetry.Enabled && len(r.metricConfig.OpenTelemetry.Exporters) == 0 && r.metricConfig.OpenTelemetry.TestReader == nil {
if endpoint := otelconfig.DefaultEndpoint(); endpoint != "" {
r.logger.Debug("Using default metrics exporter", zap.String("endpoint", endpoint))
r.metricConfig.OpenTelemetry.Exporters = append(r.metricConfig.OpenTelemetry.Exporters, &rmetric.OpenTelemetryExporter{
Endpoint: endpoint,
Exporter: otelconfig.ExporterOLTPHTTP,
HTTPPath: "/v1/metrics",
Headers: otelconfig.DefaultEndpointHeaders(r.graphApiToken),
})
}
}
var disabledFeatures []string
// The user might want to start the server with a static config
// Disable all features that requires a valid graph token and inform the user
if r.graphApiToken == "" {
r.graphqlMetricsConfig.Enabled = false
disabledFeatures = append(disabledFeatures, "Schema Usage Tracking", "Persistent operations")
if !r.developmentMode {
disabledFeatures = append(disabledFeatures, "Advanced Request Tracing")
}
if r.traceConfig.Enabled {
defaultExporter := rtrace.DefaultExporter(r.traceConfig)
if defaultExporter != nil {
disabledFeatures = append(disabledFeatures, "Cosmo Cloud Tracing")
defaultExporter.Disabled = true
}
}
if r.metricConfig.OpenTelemetry.Enabled {
defaultExporter := rmetric.GetDefaultExporter(r.metricConfig)
if defaultExporter != nil {
disabledFeatures = append(disabledFeatures, "Cosmo Cloud Metrics")
defaultExporter.Disabled = true
}
}
r.logger.Warn("No graph token provided. The following Cosmo Cloud features are disabled. Not recommended for Production.",
zap.Strings("features", disabledFeatures),
)
}
if r.persistedOperationsConfig.Safelist.Enabled && r.automaticPersistedQueriesConfig.Enabled {
return nil, errors.New("automatic persisted queries and safelist cannot be enabled at the same time (as APQ would permit queries that are not in the safelist)")
}
if r.securityConfiguration.DepthLimit != nil {
r.logger.Warn("The security configuration field 'depth_limit' is deprecated, and will be removed. Use 'security.complexity_limits.depth' instead.")
if r.securityConfiguration.ComplexityCalculationCache == nil {
r.securityConfiguration.ComplexityCalculationCache = &config.ComplexityCalculationCache{
Enabled: true,
CacheSize: r.securityConfiguration.DepthLimit.CacheSize,
}
}
if r.securityConfiguration.ComplexityLimits == nil {
r.securityConfiguration.ComplexityLimits = &config.ComplexityLimits{}
}
if r.securityConfiguration.ComplexityLimits.Depth == nil {
r.securityConfiguration.ComplexityLimits.Depth = &config.ComplexityLimit{
Enabled: r.securityConfiguration.DepthLimit.Enabled,
Limit: r.securityConfiguration.DepthLimit.Limit,
IgnorePersistedOperations: r.securityConfiguration.DepthLimit.IgnorePersistedOperations,
}
} else {
r.logger.Warn("Ignoring deprecated security configuration field 'depth_limit', in favor of the `security_complexity_limits.depth` configuration")
}
}
if r.developmentMode {
r.logger.Warn("Development mode enabled. This should only be used for testing purposes")
}
if r.healthcheck == nil {
r.healthcheck = health.New(&health.Options{
Logger: r.logger,
})
}
for _, source := range r.eventsConfig.Providers.Nats {
r.logger.Info("Nats Event source enabled", zap.String("provider_id", source.ID))
}
for _, source := range r.eventsConfig.Providers.Kafka {
r.logger.Info("Kafka Event source enabled", zap.String("provider_id", source.ID), zap.Strings("brokers", source.Brokers))
}
if !r.engineExecutionConfiguration.EnableNetPoll {
r.logger.Warn("Net poller is disabled by configuration. Falling back to less efficient connection handling method.")
} else if err := netpoll.Supported(); err != nil {
// Disable netPoll if it's not supported. This flag is used everywhere to decide whether to use netPoll or not.
r.engineExecutionConfiguration.EnableNetPoll = false
if errors.Is(err, netpoll.ErrUnsupported) {
r.logger.Warn(
"Net poller is only available on Linux and MacOS. Falling back to less efficient connection handling method.",
zap.Error(err),
)
} else {
r.logger.Warn(
"Net poller is not functional by the environment. Ensure that the system supports epoll/kqueue and that necessary syscall permissions are granted. Falling back to less efficient connection handling method.",
zap.Error(err),
)
}
}
if r.hostName == "" {
r.hostName, err = os.Hostname()
if err != nil {
r.logger.Warn("Failed to get hostname", zap.Error(err))
}
}
return r, nil
}