539 words, 3 min read

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.