If you have an URL, you sometimes need to be able to remove items from the query string given one or more specific prefixes. A common use-case is for example to remove all the analytics parameters from a URL (which usually start with the prefix utm_).

In Elixir, you can use the URI module to parse the URL and modify the query parameters. Here's a function that takes a URL, a list of prefixes, and removes all query parameters that start with any of the specified prefixes:

defmodule UrlCleaner do
  def remove_params_with_prefix(url, prefixes) do
    uri = URI.parse(url)

    params =
      parse_query_params(uri.query)
      |> Enum.reject(fn {key, _value} ->
        Enum.any?(prefixes, &starts_with_case_insensitive?(key, &1))
      end)

    uri
    |> encode_query_params(params)
    |> URI.to_string()
  end

  defp parse_query_params(nil), do: []
  defp parse_query_params(""), do: []
  defp parse_query_params(params), do: params |> URI.decode_query()

  defp encode_query_params(uri, []), do: uri |> Map.put(:query, nil)
  defp encode_query_params(uri, params), do: uri |> Map.put(:query, URI.encode_query(params))

  defp starts_with_case_insensitive?(key, prefix) do
    String.starts_with?(String.downcase(key), String.downcase(prefix))
  end
end

The main function in this module, remove_params_with_prefix/2, takes a URL and a list of prefixes as input. It returns the URL with query parameters that match any of the specified prefixes removed. This operation is case-insensitive, ensuring a robust solution for cleaning up query strings.

Here is how it works:

  1. Parsing the URL: the function starts by parsing the given URL into a %URI{} struct using Elixir's built-in URI.parse/1. This makes it easier to manipulate different parts of the URL, such as the query string.

    uri = URI.parse(url)
    
  2. Extracting and Filtering Parameters: the query string (uri.query) is processed into a map of key-value pairs using the helper function parse_query_params/1.

    • If the query string is nil or empty, it returns an empty list.
    • Otherwise, it decodes the query string into a key-value map.

    Once the parameters are parsed, Enum.reject/2 is used to filter out any parameters where the key starts with one of the specified prefixes. The starts_with_case_insensitive?/2 helper ensures the comparison is case-insensitive.

    params =
      parse_query_params(uri.query)
      |> Enum.reject(fn {key, _value} ->
        Enum.any?(prefixes, &starts_with_case_insensitive?(key, &1))
      end)
    
  3. Rebuilding the URL: after filtering, the query parameters are re-encoded into the URL using another helper function, encode_query_params/2.

    • If no parameters remain, the query string is removed by setting it to nil.
    • Otherwise, the parameters are encoded back into a query string using URI.encode_query/1.
    uri
    |> encode_query_params(params)
    |> URI.to_string()
    

Let’s look at the helper functions in detail:

  • parse_query_params/1: converts the query string into a map of key-value pairs. It handles cases where the query is nil or empty gracefully by returning an empty list.

    defp parse_query_params(nil), do: []
    defp parse_query_params(""), do: []
    defp parse_query_params(params), do: params |> URI.decode_query()
    
  • encode_query_params/2: rebuilds the query string after filtering. If the filtered parameters are empty, the query is set to nil. Otherwise, it encodes the remaining parameters into a query string.

    defp encode_query_params(uri, []), do: uri |> Map.put(:query, nil)
    defp encode_query_params(uri, params), do: uri |> Map.put(:query, URI.encode_query(params))
    
  • starts_with_case_insensitive?/2: compares the beginning of a string (key) with a prefix in a case-insensitive manner using String.downcase/1.

    defp starts_with_case_insensitive?(key, prefix) do
      String.starts_with?(String.downcase(key), String.downcase(prefix))
    end
    

Here's how this function can be used:

url = "https://example.com?utm_source=google&ref=homepage&id=123"
prefixes = ["utm_", "ref"]

cleaned_url = UrlCleaner.remove_params_with_prefix(url, prefixes)
IO.puts(cleaned_url) 
# Output: "https://example.com?id=123"

Feel free to adapt this module to handle additional use cases, such as preserving specific parameters or adding logging for the removed keys.