URI 모듈
URI 모듈
URI를 파싱하거나 쿼리 문자열을 인코딩해야 하는 순간이 왔을 때 쓰는 모듈이 바로 URI예요. 이 모듈은 URI를 다루기 위한 함수들을 모아 놓은 곳으로, 특히 런타임에 URI를 직접 만들고 조작해야 할 때 유용해요.
본문
URI 모듈은 URI를 다루기 위한 함수(예: URI 파싱, 쿼리 문자열 인코딩)를 제공해요. 이 모듈의 함수들은 RFC 3986에 따라 구현됐고, "application/x-www-form-urlencoded" 세그먼트를 처리하는 추가 기능도 제공해요.
추가로 Erlang의 :uri_string 모듈은 RFC 3986을 준수하는 URI 정규화 같은 추가 기능을 제공해요.
URI 구조체 (The URI struct)
[scheme]://[userinfo]@[host]:[port][path]?[query]#[fragment]
필드 이름을 대괄호로 표기한 위 URI 표현에 맞춰 구조체 필드가 정의돼 있어요. 필드들은 URI에 나타나는 그대로의 인코딩된 URI 구성 요소를 담아요. 예를 들어 userinfo 안의 슬래시는 /가 아니라 %2F로 저장해야 해요. parse/1이나 new/1 같은 함수는 그 필드들의 기존 퍼센트 인코딩 시퀀스를 보존하고, to_string/1 같은 함수는 필드들이 이미 필요에 따라 인코딩돼 있다고 가정해요. 필드를 직접 설정하거나 수정할 때는 그에 맞게 인코딩해 줘야 해요.
참고로 authority 필드는 deprecated(더 이상 권장되지 않음) 상태예요. parse/1은 하위 호환성을 위해 여전히 이 필드를 채워 주지만, 일반적으로는 설정하거나 읽는 것을 피해야 해요.
구조체 타입은 다음과 같아요.
@type t() :: %URI{
authority: authority(),
fragment: nil | binary(),
host: nil | binary(),
path: nil | binary(),
port: nil | :inet.port_number(),
query: nil | binary(),
scheme: nil | binary(),
userinfo: nil | binary()
}
추가로 authority() 타입은 deprecated 상태예요.
@type authority() :: nil | binary()
append_path(uri, path) (since 1.15.0)
주어진 uri에 path를 이어 붙여요.
@spec append_path(t(), String.t()) :: t()
path는 /로 시작해야 하고, 프래그먼트나 쿼리 문자열 같은 추가 URL 구성 요소를 담을 수 없어요. 이 함수는 나아가 그 경로가 유효하며 쿼리 문자열이나 프래그먼트 부분을 포함하지 않는다고 가정해요.
iex> URI.append_path(URI.parse("http://example.com/foo/?x=1"), "/my-path") |> URI.to_string()
"http://example.com/foo/my-path?x=1"
iex> URI.append_path(URI.parse("http://example.com"), "my-path")
** (ArgumentError) path must start with "/", got: "my-path"
append_query(uri, query) (since 1.14.0)
주어진 uri에 query를 이어 붙여요.
@spec append_query(t(), binary()) :: t()
주어진 query는 자동으로 인코딩되지 않으니, 필요하다면 encode/2나 encode_www_form/1을 사용해요.
iex> URI.append_query(URI.parse("http://example.com/"), "x=1") |> URI.to_string()
"http://example.com/?x=1"
iex> URI.append_query(URI.parse("http://example.com/?x=1"), "y=2") |> URI.to_string()
"http://example.com/?x=1&y=2"
iex> URI.append_query(URI.parse("http://example.com/?x=1"), "x=2") |> URI.to_string()
"http://example.com/?x=1&x=2"
char_reserved?(character)
URI에서 character가 예약 문자(reserved)인지 확인해요.
@spec char_reserved?(byte()) :: boolean()
RFC 3986 섹션 2.2에 명시된 대로, 다음 문자들이 예약 문자예요: :, /, ?, #, [, ], @, !, $, &, ', (, ), *, +, ,, ;, =
iex> URI.char_reserved?(?+)
true
char_unescaped?(character)
URI에서 character가 이스케이프 없이 허용되는 문자인지 확인해요.
@spec char_unescaped?(byte()) :: boolean()
이것은 URI.encode/2가 기본으로 사용하는 기준이에요. 예약 문자와 비예약 문자 모두 이스케이프 없이 유지돼요.
iex> URI.char_unescaped?(?{)
false
char_unreserved?(character)
URI에서 character가 비예약 문자(unreserved)인지 확인해요.
@spec char_unreserved?(byte()) :: boolean()
RFC 3986 섹션 2.3에 명시된 대로, 다음 문자들이 비예약 문자예요.
- 영숫자 문자:
A-Z,a-z,0-9 ~,_,-,.
iex> URI.char_unreserved?(?_)
true
decode(uri)
URI의 퍼센트 인코딩을 풀어요(Percent-unescape).
@spec decode(binary()) :: binary()
iex> URI.decode("https%3A%2F%2Felixir-lang.org")
"https://elixir-lang.org"
decode_query(query, map \ %{}, encoding \ :www_form)
query를 맵으로 디코딩해요.
@spec decode_query(binary(), %{optional(binary()) => binary()}, :rfc3986 | :www_form) ::
%{
optional(binary()) => binary()
}
key1=value1&key2=value2... 형태의 쿼리 문자열이 주어지면, 이 함수는 쿼리 문자열의 각 키-값 쌍을 주어진 map의 한 항목으로 넣어요. 결과 맵의 키와 값은 바이너리이고, 퍼센트 이스케이프가 풀려요.
다음 encoding 옵션 중 하나를 지정할 수 있어요.
:www_form- (기본값, v1.12.0부터)decode_www_form/1에 따라 키와 값을 디코딩해요. 브라우저가 쿼리 문자열과 폼 데이터에 보통 사용하는 형식이에요.+를(공백)으로 디코딩해요.:rfc3986- (v1.12.0부터)decode/1에 따라 키와 값을 디코딩해요. 결과는:www_form과 같지만, RFC 3986에 맞춰+를 그대로 남겨 둬요.
인코딩은 하위 호환성을 위해 기본값이 :www_form이에요. 각 값을 직접 순회하고 싶다면 query_decoder/1을 사용하면 돼요.
iex> URI.decode_query("foo=1&bar=2")
%{"bar" => "2", "foo" => "1"}
iex> URI.decode_query("percent=oh+yes%21", %{"starting" => "map"})
%{"percent" => "oh yes!", "starting" => "map"}
iex> URI.decode_query("percent=oh+yes%21", %{}, :rfc3986)
%{"percent" => "oh+yes!"}
decode_www_form(string)
string을 "x-www-form-urlencoded"로 디코딩해요.
@spec decode_www_form(binary()) :: binary()
참고로 "x-www-form-urlencoded"는 RFC 3986의 일부로 명시되지는 않아요. 다만 브라우저가 쿼리 문자열과 폼 데이터를 인코딩할 때 흔히 쓰는 형식이에요.
iex> URI.decode_www_form("%3Call+in%2F")
"<all in/"
default_port(scheme)
주어진 scheme에 대한 기본 포트를 반환해요.
@spec default_port(binary()) :: nil | non_neg_integer()
그 스킴이 URI 모듈에 알려지지 않았다면 nil을 반환해요. 어떤 스킴에 대한 기본 포트든 default_port/2로 전역 설정할 수 있어요.
iex> URI.default_port("ftp")
21
iex> URI.default_port("ponzi")
nil
default_port(scheme, port)
주어진 scheme에 대한 기본 port를 등록해요.
@spec default_port(binary(), non_neg_integer()) :: :ok
이 함수를 호출하면 주어진 스킴 scheme에 대해 default_port/1이 port를 반환해요. 이 함수가 주어진 scheme의 기본 포트를 전역적으로(모든 애플리케이션에 대해) 바꾼다는 점을 주의하세요.
새 URI를 등록하고 싶다면 애플리케이션의 시작(start) 콜백에서 이 함수를 호출하길 권장해요.
encode(string, predicate \ &char_unescaped?/1)
string에서 이스케이프가 필요한 모든 문자를 퍼센트 인코딩해요.
@spec encode(binary(), (byte() -> as_boolean(term()))) :: binary()
선택적 predicate 인자는 string의 어떤 바이트를 이스케이프해야 하는지 판별하는 함수를 지정해요.
- 함수가 truthy 값을 반환하면 그 바이트는 그대로 유지돼요.
- 함수가 falsy 값을 반환하면 그 바이트는 이스케이프돼요.
predicate 인자에는 몇 가지 내장 함수를 쓸 수 있어요.
URI.char_unescaped?/1(기본값) - 예약 문자(:나/같은)나 비예약 문자(문자·숫자 같은)를 그대로 유지해요. 보통 URI 전체를 인코딩할 때 사용해요.URI.char_unreserved?/1- 비예약 문자(문자·숫자 같은)를 그대로 유지해요. 보통 쿼리나 프래그먼트 같은 URI의 구성 요소를 인코딩할 때 사용해요.URI.char_reserved?/1- 예약 문자(:나/같은)를 그대로 유지해요.
물론 커스텀 함수를 사용할 수도 있어요. string을 "x-www-form-urlencoded"로 인코딩하는 데 관심이 있다면 encode_www_form/1을 참고하세요.
iex> URI.encode("ftp://s-ite.tld/?value=put it+й")
"ftp://s-ite.tld/?value=put%20it+%D0%B9"
iex> URI.encode("a string", &(&1 != ?i))
"a str%69ng"
encode_query(enumerable, encoding \ :www_form)
enumerable을 encoding을 사용해 쿼리 문자열로 인코딩해요.
@spec encode_query(Enumerable.t(), :rfc3986 | :www_form) :: binary()
두 요소짜리 튜플의 리스트로 열거되는 enumerable(예: 맵이나 키워드 리스트)을 받아 key1=value1&key2=value2... 형태의 문자열을 반환해요.
키와 값은 String.Chars 프로토콜을 구현하는 어떤 term이든 될 수 있는데, 리스트는 명시적으로 금지돼요.
다음 encoding 전략 중 하나를 지정할 수 있어요.
:www_form- (기본값, v1.12.0부터)encode_www_form/1에 따라 키와 값을 URL 인코딩해요. 브라우저가 쿼리 문자열과 폼 데이터에 보통 사용하는 형식이에요.(공백)을+로 인코딩해요.:rfc3986- (v1.12.0부터):www_form과 같지만, RFC 3986에 따라(공백)을%20으로 인코딩해요. 브라우저가 아닌 환경에서 인코딩한다면 가장 좋은 선택이에요. 공백을+로 인코딩하면 URI 파서에 모호함을 줄 수 있고, 공백이 리터럴 플러스 기호로 해석될 수 있기 때문이에요.
인코딩은 하위 호환성을 위해 기본값이 :www_form이에요.
iex> query = %{"foo" => 1, "bar" => 2}
iex> URI.encode_query(query)
"bar=2&foo=1"
iex> query = %{"key" => "value with spaces"}
iex> URI.encode_query(query)
"key=value+with+spaces"
iex> query = %{"key" => "value with spaces"}
iex> URI.encode_query(query, :rfc3986)
"key=value%20with%20spaces"
iex> URI.encode_query(%{key: [:a, :list]})
** (ArgumentError) encode_query/2 values cannot be lists, got: [:a, :list]
encode_www_form(string)
string을 "x-www-form-urlencoded"로 인코딩해요.
@spec encode_www_form(binary()) :: binary()
참고로 "x-www-form-urlencoded"는 RFC 3986의 일부로 명시되지는 않아요. 다만 브라우저가 쿼리 문자열과 폼 데이터를 인코딩할 때 흔히 쓰는 형식이에요.
iex> URI.encode_www_form("put: it+й")
"put%3A+it%2B%D0%B9"
merge(uri, rel)
두 URI를 합쳐요.
@spec merge(t() | binary(), t() | binary()) :: t()
이 함수는 RFC 3986 섹션 5.2에 따라 두 URI를 병합해요.
iex> URI.merge(URI.parse("http://google.com"), "/query") |> to_string()
"http://google.com/query"
iex> URI.merge("http://example.com", "http://google.com") |> to_string()
"http://google.com"
new(uri) (since 1.13.0)
문자열을 파싱·검증하거나 기존 URI로부터 새 URI 구조체를 만들어요.
@spec new(t() | String.t()) :: {:ok, t()} | {:error, String.t()}
%URI{} 구조체가 주어지면 그대로 {:ok, uri}를 반환해요. 문자열이 주어지면 파싱하고 검증해요. 문자열이 유효하면 {:ok, uri}를, 그렇지 않으면 URI의 잘못된 부분과 함께 {:error, part}를 반환해요. 추가 검증 없이 URI를 파싱하려면 parse/1을 참고하세요.
이 함수는 절대 URL과 상대 URL을 모두 파싱할 수 있어요. scheme 필드가 nil인지 아닌지로 URI가 절대인지 상대인지 확인할 수 있어요.
포트 없이 URI가 주어지면 그 URI 스킴에 대한 URI.default_port/1이 반환하는 값이 :port 필드에 사용돼요. 스킴도 소문자로 정규화돼요.
브라우저 호환성 (Browser compatibility)
이 함수는 브라우저와 같은 파싱 규칙을 따르지 않아요. 브라우저는 WHATWG URL 표준을 따르는 반면, 이 함수는 RFC 3986을 구현해요. 둘 사이의 동작 차이를 예상해야 해요. 이 함수는 브라우저가 허용하는 URL(브라우저는 관대한 경향이 있음)을 종종 거부하기도 해요. 흔한 예로 쿼리 문자열의 인코딩되지 않은 대괄호(foo[bar]=baz 같은)와 경로 구분자로 쓰인 백슬래시가 있어요.
iex> URI.new("https://elixir-lang.org/")
{:ok, %URI{
fragment: nil,
host: "elixir-lang.org",
path: "/",
port: 443,
query: nil,
scheme: "https",
userinfo: nil
}}
iex> URI.new("//elixir-lang.org/")
{:ok, %URI{
fragment: nil,
host: "elixir-lang.org",
path: "/",
port: nil,
query: nil,
scheme: nil,
userinfo: nil
}}
iex> URI.new("/foo/bar")
{:ok, %URI{
fragment: nil,
host: nil,
path: "/foo/bar",
port: nil,
query: nil,
scheme: nil,
userinfo: nil
}}
iex> URI.new("foo/bar")
{:ok, %URI{
fragment: nil,
host: nil,
path: "foo/bar",
port: nil,
query: nil,
scheme: nil,
userinfo: nil
}}
iex> URI.new("//[fe80::]/")
{:ok, %URI{
fragment: nil,
host: "fe80::",
path: "/",
port: nil,
query: nil,
scheme: nil,
userinfo: nil
}}
iex> URI.new("https:?query")
{:ok, %URI{
fragment: nil,
host: nil,
path: nil,
port: 443,
query: "query",
scheme: "https",
userinfo: nil
}}
iex> URI.new("/invalid_greater_than_in_path/>")
{:error, ">"}
기존 URI를 주면 그냥 튜플에 감싸 반환해요.
iex> {:ok, uri} = URI.new("https://elixir-lang.org/")
iex> URI.new(uri)
{:ok, %URI{
fragment: nil,
host: "elixir-lang.org",
path: "/",
port: 443,
query: nil,
scheme: "https",
userinfo: nil
}}
new!(uri) (since 1.13.0)
new/1과 비슷하지만, 유효하지 않은 문자열이 주어지면 URI.Error를 던져요.
@spec new!(t() | String.t()) :: t()
iex> URI.new!("https://elixir-lang.org/")
%URI{
fragment: nil,
host: "elixir-lang.org",
path: "/",
port: 443,
query: nil,
scheme: "https",
userinfo: nil
}
iex> URI.new!("/invalid_greater_than_in_path/>")
** (URI.Error) cannot parse due to reason invalid_uri: ">"
기존 URI를 주면 그냥 반환해요.
iex> uri = URI.new!("https://elixir-lang.org/")
iex> URI.new!(uri)
%URI{
fragment: nil,
host: "elixir-lang.org",
path: "/",
port: 443,
query: nil,
scheme: "https",
userinfo: nil
}
parse(uri)
추가 검증 없이 URI를 구성 요소로 파싱해요.
@spec parse(t() | binary()) :: t()
이 함수는 절대 URL과 상대 URL을 모두 파싱할 수 있어요. scheme 필드가 nil인지 아닌지로 URI가 절대인지 상대인지 확인할 수 있어요.
이 함수는 절대·상대 URI 모두 잘 구성돼 있다고 가정하며, 어떤 검증도 수행하지 않아요. 단순히 URL을 부분으로 쪼갤 뿐이에요. 파싱 후 URI 필드를 검증하려면 new/1을 사용하세요.
포트 없이 URI가 주어지면 그 URI 스킴에 대한 URI.default_port/1이 반환하는 값이 :port 필드에 사용돼요. 스킴도 소문자로 정규화돼요.
이 함수에 URI 구조체가 주어지면 수정하지 않고 그대로 반환해요.
브라우저 호환성 (Browser compatibility)
이 함수는 브라우저와 같은 파싱 규칙을 따르지 않아요. 브라우저는 WHATWG URL 표준을 따르는 반면, 이 함수는 RFC 3986을 구현해요. 특히 모서리 케이스에서 둘 사이의 동작 차이를 예상해야 해요.
:authority 필드
이 함수는 하위 호환성 때문에 deprecated 필드인 :authority를 설정해요.
iex> URI.parse("https://elixir-lang.org/")
%URI{
authority: "elixir-lang.org",
fragment: nil,
host: "elixir-lang.org",
path: "/",
port: 443,
query: nil,
scheme: "https",
userinfo: nil
}
iex> URI.parse("//elixir-lang.org/")
%URI{
authority: "elixir-lang.org",
fragment: nil,
host: "elixir-lang.org",
path: "/",
port: nil,
query: nil,
scheme: nil,
userinfo: nil
}
iex> URI.parse("/foo/bar")
%URI{
fragment: nil,
host: nil,
path: "/foo/bar",
port: nil,
query: nil,
scheme: nil,
userinfo: nil
}
iex> URI.parse("foo/bar")
%URI{
fragment: nil,
host: nil,
path: "foo/bar",
port: nil,
query: nil,
scheme: nil,
userinfo: nil
}
URI.new/1과 달리 이 함수는 검증을 수행하지 않으므로, 잘못 구성된 URI도 받아들여 구성 요소로 쪼개요. 예를 들어:
iex> URI.parse("/invalid_greater_than_in_path/>")
%URI{
fragment: nil,
host: nil,
path: "/invalid_greater_than_in_path/>",
port: nil,
query: nil,
scheme: nil,
userinfo: nil
}
또 다른 예로 쿼리 문자열에 대괄호가 있는 URI가 있어요. 위와 같은 이유(검증 없음)로 parse/1은 받아들이지만 new/1은 거부해요.
iex> URI.parse("/?foo[bar]=baz")
%URI{
fragment: nil,
host: nil,
path: "/",
port: nil,
query: "foo[bar]=baz",
scheme: nil,
userinfo: nil
}
일반적으로 말해 대괄호는 브라우저가 쓰는 WHATWG URL 표준에서도 엄격히 유효하지 않지만, 브라우저는 관대해서 유효하지 않은 문자가 그대로 통과하도록 허용하는 경우가 많아요.
query_decoder(query, encoding \ :www_form)
주어진 query의 키-값 쌍을 나타내는 두 요소 튜플의 스트림을 반환해요.
@spec query_decoder(binary(), :rfc3986 | :www_form) :: Enumerable.t()
각 튜플의 키와 값은 바이너리이고 퍼센트 이스케이프가 풀려요.
다음 encoding 옵션 중 하나를 지정할 수 있어요.
:www_form- (기본값, v1.12.0부터)decode_www_form/1에 따라 키와 값을 디코딩해요. 브라우저가 쿼리 문자열과 폼 데이터에 보통 사용하는 형식이에요.+를(공백)으로 디코딩해요.:rfc3986- (v1.12.0부터)decode/1에 따라 키와 값을 디코딩해요. 결과는:www_form과 같지만, RFC 3986에 맞춰+를 그대로 남겨 둬요.
인코딩은 하위 호환성을 위해 기본값이 :www_form이에요.
iex> URI.query_decoder("foo=1&bar=2") |> Enum.to_list()
[{"foo", "1"}, {"bar", "2"}]
iex> URI.query_decoder("food=bread%26butter&drinks=tap%20water+please") |> Enum.to_list()
[{"food", "bread&butter"}, {"drinks", "tap water please"}]
iex> URI.query_decoder("food=bread%26butter&drinks=tap%20water+please", :rfc3986) |> Enum.to_list()
[{"food", "bread&butter"}, {"drinks", "tap water+please"}]
to_string(uri)
주어진 URI 구조체의 문자열 표현을 반환해요.
@spec to_string(t()) :: binary()
이 함수는 각 필드가 유효하고 parse/1과 new/1이 하는 것처럼 이스케이프됐다고 가정하고 URI 구성 요소를 문자열로 조립해요.
iex> uri = URI.parse("http://google.com")
iex> URI.to_string(uri)
"http://google.com"
iex> uri = URI.parse("foo://bar.baz")
iex> URI.to_string(uri)
"foo://bar.baz"