Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ class AgentFactory
attr_reader :customizer, :container, :has_env_secret
attr_accessor :schema_only_mode

ENV_SECRET_FORMAT = /\A[0-9a-f]{64}\z/

def initialize
super
@reloading = false
end

def setup(options)
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
@options = options
@has_env_secret = options.to_h.key?(:env_secret)
@has_env_secret = !options.to_h[:env_secret].nil?
@customizer = ForestAdminDatasourceCustomizer::DatasourceCustomizer.new
build_container
build_cache
Expand Down Expand Up @@ -115,6 +117,7 @@ def send_schema(force: false)
end

return unless @has_env_secret
return unless secrets_format_valid?

schema = generate_schema_file

Expand All @@ -127,7 +130,19 @@ def send_schema(force: false)
end
end

post_schema(schema, force)
begin
post_schema(schema, force)
rescue StandardError => e
# A well-formed secret can still be rejected by the server (wrong project, revoked
# secret, network down, ...). Same rule as an invalid format: block boot in
# production, but never in dev - warn and move on instead. Scoped to this call
# only, so a local bug (a broken customization, an unwritable schema_path, ...)
# still surfaces immediately instead of being logged as a generic warning.
raise e if Facades::Container.cache(:is_production)

@logger.log('Warn', "[ForestAdmin] #{e.message}")
@logger.log('Warn', '[ForestAdmin] Schema sync failed, continuing without it.')
end
end

# Generates or loads the schema and writes it to file (in development mode).
Expand Down Expand Up @@ -175,6 +190,40 @@ def write_schema_file(schema_path, schema)

private

# Checked from send_schema rather than setup, so that schema-only mode (which never
# syncs to the server) never fails on a secret it doesn't actually need.
def secrets_format_valid?
errors = secret_format_errors
return true if errors.empty?

if Facades::Container.cache(:is_production)
raise ForestAdminAgent::Http::Exceptions::ValidationError, errors.join(' ')
end

# Don't block boot on a config mistake in dev: warn loudly and skip the schema
# sync instead, so the developer can still work on the rest of the app.
errors.each { |error| @logger.log('Warn', "[ForestAdmin] #{error}") }
@logger.log('Warn', '[ForestAdmin] Skipping schema sync until this is fixed.')
false
end

def secret_format_errors
errors = []
env_secret = @options.to_h[:env_secret]

unless env_secret.is_a?(String) && env_secret.match?(ENV_SECRET_FORMAT)
errors << 'config.env_secret is invalid: it must be the 64-character hexadecimal secret from your ' \
'Forest Admin project settings.'
end

auth_secret = @options.to_h[:auth_secret]
unless auth_secret.is_a?(String)
errors << 'config.auth_secret is invalid: it must be a string. Any long random value works.'
end

errors
end

def container_replace(key, value)
@container._container.delete(key.to_s)
@container.register(key, value)
Expand Down Expand Up @@ -211,6 +260,7 @@ def do_server_want_schema(hash)

begin
response = client.post('/forest/apimaps/hashcheck', { schemaFileHash: hash }.to_json)
client.raise_for_response!(response)
body = JSON.parse(response.body)
body['sendSchema']
rescue JSON::ParserError => e
Expand Down Expand Up @@ -281,7 +331,8 @@ def log_schema_skip
def send_schema_to_server(api_map)
ForestAdminAgent::Facades::Container.logger.log('Info', 'schema was updated, sending new version')
client = ForestAdminAgent::Http::ForestAdminApiRequester.new
client.post('/forest/apimaps', api_map.to_json)
response = client.post('/forest/apimaps', api_map.to_json)
client.raise_for_response!(response)
rescue Faraday::Error => e
status = e.response[:status] if e.response
if status
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,33 +41,48 @@ def handle_response_error(error)
)
end

if error.response[:status].zero? || error.response[:status] == 502
raise_for_status(error.response[:status], cause: error)
end

# Faraday does not raise on HTTP error statuses here (no raise_error middleware
# configured), so a response has to be checked explicitly to turn e.g. an
# invalid envSecret (404) into the same typed errors as handle_response_error.
def raise_for_response!(response)
return if response.success?

raise_for_status(response.status, message: response.reason_phrase)
end

private

def raise_for_status(status, cause: nil, message: cause&.message)
if status.zero? || status == 502
raise BadGatewayError.new(
'Failed to reach ForestAdmin server. Are you online?',
details: { status: error.response[:status] },
cause: error
details: { status: status },
cause: cause
)
end

if error.response[:status] == 404
if status == 404
raise NotFoundError.new(
'ForestAdmin server failed to find the project related to the envSecret you configured. Can you check that you copied it properly in the Forest initialization?',
details: { status: error.response[:status] }
details: { status: status }
)
end

if error.response[:status] == 503
if status == 503
raise ServiceUnavailableError.new(
'Forest is in maintenance for a few minutes. We are upgrading your experience in the forest. We just need a few more minutes to get it right.',
details: { status: error.response[:status] },
cause: error
details: { status: status },
cause: cause
)
end

raise InternalServerError.new(
'An unexpected error occurred while contacting the ForestAdmin server. Please contact support@forestadmin.com for further investigations.',
details: { status: error.response[:status], message: error.message },
cause: error
details: { status: status, message: message },
cause: cause
)
end
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
end
Expand Down
Loading
Loading