Recommendations for where to check that certain config values are defined?

How should missing configuration values be handled? I am working with an app that needs a few different system environment variables set in order for it to work. Writing a test does not work because the test.exs config file often has sample values defined.

I’ve tried doing something like this:

defmodule MyApp do
    Application.get_env(:my_app, :something) || raise "Something not defined"
end

where my config looks like this:

import Config

config :my_app, something: System.get_env("SOMETHING")

Is that a viable way to do this?

Not sure to understand what you need but maybe if you use System.fetch_env!/1 instead of System.get_env/1 you will get what you want.

If you use System.fetch_env!/1 in your config file, you will get an error at compile time if "SOMETHING" not exported.

I will give that a try. Thank you

Unfortunately, that did not work the way I expected. For example, if I put something like this in my config.exs:

config :my_app, xxx: System.fetch_env!("XXX")

and I have a default value in an environment-specific config file like dev.exs:

config :my_app, xxx: "default_xxx"

the exception is always raised, even when xxx has a value for the current environment. I suppose that makes sense: the System.fetch_env!/1 function is always executed when it appears in the config.exs.

So perhaps this function is a viable solution if I never use it in the config.exs and only in the environment-specific configs.

This last piece can be explained by the fact that config.exs is evaluated in its entirety first, up until it hits the import_config line, and then that file is evaluated in its entirety, so the System.fetch_env! call happens and fails before the environment-specific file is able to override the value.

You don’t mention whether or not you’re using releases, but that is a closely related topic to this question and the documentation there might offer some guidance that you haven’t encountered up until now.

1 Like

In my config I use helpers to give me more confidence that I get the environments variables:

import Config

################################################################################
# START HELPERS
################################################################################
#
# Getting environments values from the environment as it his now is flaky,
#  because we may get a blank string "   ", that will be accepted as valid value
#  when using the recommended approach:
#
#  ```
#  System.get_env(var) || raise "blah blah"
#  ```
#
# Also if an env var is assigned with double quotes around it's value, then
#  `System.get_env(var)` will not strip them out, thus we end-up with an
#  unexpected value in our application.
#
# Sometimes we also want to enforce that the value in the env var has a minimal
#  length, like for secrets, thus I prefer to have this being enforced in the
#  moment we fetch it.
#

blank? =  fn
            <<" " :: binary, rest :: binary>>, func -> func.(rest, func)
            _string = "", _func -> true
            _string, _func -> false
          end

empty? =  fn string -> blank?.(string, blank?) end

single_quoted? =  fn string ->
                    ("\'" === binary_part(string, 0, 1))
                    and ("\'" === binary_part(string, byte_size(string), -1))
                  end

double_quoted? =  fn string ->
                    ("\"" === binary_part(string, 0, 1))
                    and ("\"" === binary_part(string, byte_size(string), -1))
                  end

get_env = fn (var, min_length)
  when is_binary(var) and is_integer(min_length) ->
    case System.get_env(var) do
      nil ->
        raise "Missing value for var #{var} in your .env"

      value when is_binary(value) ->
        with false <- single_quoted?.(value),
             false <- double_quoted?.(value),
             false <- empty?.(value),
             true <- byte_size(value) >= min_length
          do
            value
          else
            true ->
              raise "#{var} value cannot be surrounded by single quotes: #{value}"

            true ->
              raise "#{var} value cannot be surrounded by double quotes: #{value}"

            true ->
              raise "#{var} cannot be empty"

            false ->
              raise "Value for env var #{var} needs to have a minimal length of: #{min_length}"
        end
    end
end

################################################################################
# END HELPERS
################################################################################

config :tasks,
  ecto_repos: [Tasks.Repo]

# Configures the endpoint
config :tasks, TasksWeb.Endpoint,
  url: [host: "localhost"],
  secret_key_base: get_env.("SECRET_KEY_BASE", 255),
  render_errors: [view: TasksWeb.ErrorView, accepts: ~w(html json)],
  pubsub_server: Tasks.PubSub,
  live_view: [
    signing_salt: get_env.("LIVE_VIEW_SIGNING_SALT", 128)
  ],
  session_options: [
    store: :cookie,
    key: "_tasks_key",
    signing_salt: get_env.("SESSION_SIGNING_SALT", 128),
    encryption_salt: get_env.("SESSION_ENCRYPTION_SALT", 128)
  ]

But I am not planning to use the default config flow config.exs > prod.exs > prod.secret.exs > releases.exs, instead I am planning in use release.exs > config.exs, that in dev becomes release.exs > config.exs > dev.exs.

NOTE: I have not deploy yet with releases.exs, because this config flow is a work I have in progress now, thus it may not work for deploying with releases, but it works in development. I will let you know when a deploy with it.

config.exs and releases.exs are handled completely separately. config.exs is called by mix (and it further includes dev/test/prod.exs), while releases.exs is called by the release startup scripts.

Not on my setup that I am trialing…

My config.exs imports the releases.exs, but as I said this is working on development, but nor tested yet to make a release.

config.exs:

# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.

# General application configuration
use Mix.Config

# Keep consistency between environments. Development will derive from a
#  production environment, not the other way around. Having separated
#  configuration as Elixir and Phoenix currently promote can lead to adding a
#  new configuration field in `dev.exs` that you then forgot to add in
#  `prod.exs` and/or `releases.exs`, therefore you then have a broken production
#  deployment.
import_config "releases.exs"

config :mnesia,
  dir: '.mnesia/#{Mix.env}/#{node()}'

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"