diff --git a/rust-runtime/inlineable/Cargo.toml b/rust-runtime/inlineable/Cargo.toml
index 49c1d3b3e3..280441c8f9 100644
--- a/rust-runtime/inlineable/Cargo.toml
+++ b/rust-runtime/inlineable/Cargo.toml
@@ -23,6 +23,7 @@ repository = "https://github.com/awslabs/smithy-rs"
 "pin-project-lite" = "0.2"
 "tower" = { version = "0.4.11", default_features = false }
 "async-trait" = "0.1"
+"url" = "2.2.2"
 
 [dev-dependencies]
 proptest = "1"
diff --git a/rust-runtime/inlineable/src/endpoint_lib.rs b/rust-runtime/inlineable/src/endpoint_lib.rs
new file mode 100644
index 0000000000..395027d517
--- /dev/null
+++ b/rust-runtime/inlineable/src/endpoint_lib.rs
@@ -0,0 +1,10 @@
+/*
+ *  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *  SPDX-License-Identifier: Apache-2.0
+ */
+
+mod arn;
+mod diagnostic;
+mod host;
+mod parse_url;
+mod substring;
diff --git a/rust-runtime/inlineable/src/endpoint_lib/arn.rs b/rust-runtime/inlineable/src/endpoint_lib/arn.rs
new file mode 100644
index 0000000000..03ec56b9ee
--- /dev/null
+++ b/rust-runtime/inlineable/src/endpoint_lib/arn.rs
@@ -0,0 +1,153 @@
+/*
+ *  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *  SPDX-License-Identifier: Apache-2.0
+ */
+
+use crate::endpoint_lib::diagnostic::DiagnosticCollector;
+use std::borrow::Cow;
+use std::error::Error;
+use std::fmt::{Display, Formatter};
+
+#[derive(Debug, Eq, PartialEq)]
+pub(crate) struct Arn<'a> {
+    partition: &'a str,
+    service: &'a str,
+    region: &'a str,
+    account_id: &'a str,
+    resource_id: Vec<&'a str>,
+}
+
+#[allow(unused)]
+impl<'a> Arn<'a> {
+    pub(crate) fn partition(&self) -> &'a str {
+        self.partition
+    }
+    pub(crate) fn service(&self) -> &'a str {
+        self.service
+    }
+    pub(crate) fn region(&self) -> &'a str {
+        self.region
+    }
+    pub(crate) fn account_id(&self) -> &'a str {
+        self.account_id
+    }
+    pub(crate) fn resource_id(&self) -> &Vec<&'a str> {
+        &self.resource_id
+    }
+}
+
+#[derive(Debug, PartialEq)]
+pub(crate) struct InvalidArn {
+    message: Cow<'static, str>,
+}
+
+impl InvalidArn {
+    fn from_static(message: &'static str) -> InvalidArn {
+        Self {
+            message: Cow::Borrowed(message),
+        }
+    }
+}
+impl Display for InvalidArn {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.message)
+    }
+}
+impl Error for InvalidArn {}
+
+impl<'a> Arn<'a> {
+    pub(crate) fn parse(arn: &'a str) -> Result<Self, InvalidArn> {
+        let mut split = arn.splitn(6, ':');
+        let invalid_format =
+            || InvalidArn::from_static("ARN must have 6 components delimited by `:`");
+        let arn = split.next().ok_or_else(invalid_format)?;
+        let partition = split.next().ok_or_else(invalid_format)?;
+        let service = split.next().ok_or_else(invalid_format)?;
+        let region = split.next().ok_or_else(invalid_format)?;
+        let account_id = split.next().ok_or_else(invalid_format)?;
+        let resource_id = split.next().ok_or_else(invalid_format)?;
+
+        if arn != "arn" {
+            return Err(InvalidArn::from_static(
+                "first component of the ARN must be `arn`",
+            ));
+        }
+        if partition.is_empty() || service.is_empty() || resource_id.is_empty() {
+            return Err(InvalidArn::from_static(
+                "partition, service, and resource id must all be non-empty",
+            ));
+        }
+
+        let resource_id = resource_id.split([':', '/']).collect::<Vec<_>>();
+        Ok(Self {
+            partition,
+            service,
+            region,
+            account_id,
+            resource_id,
+        })
+    }
+}
+
+pub(crate) fn parse_arn<'a, 'b>(input: &'a str, e: &'b mut DiagnosticCollector) -> Option<Arn<'a>> {
+    e.capture(Arn::parse(input))
+}
+
+#[cfg(test)]
+mod test {
+    use super::Arn;
+    use crate::endpoint_lib::diagnostic::DiagnosticCollector;
+
+    #[test]
+    fn arn_parser() {
+        let arn = "arn:aws:s3:us-east-2:012345678:outpost:op-1234";
+        let parsed = Arn::parse(arn).expect("valid ARN");
+        assert_eq!(
+            parsed,
+            Arn {
+                partition: "aws",
+                service: "s3",
+                region: "us-east-2",
+                account_id: "012345678",
+                resource_id: vec!["outpost", "op-1234"]
+            }
+        );
+    }
+
+    #[test]
+    fn allow_slash_arns() {
+        let arn = "arn:aws:s3:us-east-2:012345678:outpost/op-1234";
+        let parsed = Arn::parse(arn).expect("valid ARN");
+        assert_eq!(
+            parsed,
+            Arn {
+                partition: "aws",
+                service: "s3",
+                region: "us-east-2",
+                account_id: "012345678",
+                resource_id: vec!["outpost", "op-1234"]
+            }
+        );
+    }
+
+    #[test]
+    fn resource_id_must_be_nonempty() {
+        let arn = "arn:aws:s3:us-east-2:012345678:";
+        Arn::parse(arn).expect_err("empty resource");
+    }
+
+    #[test]
+    fn arns_with_empty_parts() {
+        let arn = "arn:aws:s3:::my_corporate_bucket/Development/*";
+        assert_eq!(
+            Arn::parse(arn).expect("valid arn"),
+            Arn {
+                partition: "aws",
+                service: "s3",
+                region: "",
+                account_id: "",
+                resource_id: vec!["my_corporate_bucket", "Development", "*"]
+            }
+        );
+    }
+}
diff --git a/rust-runtime/inlineable/src/endpoint_lib/diagnostic.rs b/rust-runtime/inlineable/src/endpoint_lib/diagnostic.rs
new file mode 100644
index 0000000000..1b39e19ba8
--- /dev/null
+++ b/rust-runtime/inlineable/src/endpoint_lib/diagnostic.rs
@@ -0,0 +1,45 @@
+/*
+ *  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *  SPDX-License-Identifier: Apache-2.0
+ */
+
+use std::error::Error;
+
+/// Diagnostic collector for endpoint resolution
+///
+/// Endpoint functions return `Option<T>`—to enable diagnostic information to flow, we capture the
+/// last error that occurred.
+#[derive(Debug, Default)]
+pub(crate) struct DiagnosticCollector {
+    last_error: Option<Box<dyn Error + Send + Sync>>,
+}
+
+impl DiagnosticCollector {
+    /// Report an error to the collector
+    pub(crate) fn report_error(&mut self, err: impl Into<Box<dyn Error + Send + Sync>>) {
+        self.last_error = Some(err.into());
+    }
+
+    /// Capture a result, returning Some(t) when the input was `Ok` and `None` otherwise
+    pub(crate) fn capture<T, E: Into<Box<dyn Error + Send + Sync>>>(
+        &mut self,
+        err: Result<T, E>,
+    ) -> Option<T> {
+        match err {
+            Ok(res) => Some(res),
+            Err(e) => {
+                self.report_error(e);
+                None
+            }
+        }
+    }
+
+    pub(crate) fn take_last_error(&mut self) -> Option<Box<dyn Error + Send + Sync>> {
+        self.last_error.take()
+    }
+
+    /// Create a new diagnostic collector
+    pub(crate) fn new() -> Self {
+        Self { last_error: None }
+    }
+}
diff --git a/rust-runtime/inlineable/src/endpoint_lib/host.rs b/rust-runtime/inlineable/src/endpoint_lib/host.rs
new file mode 100644
index 0000000000..389b1da0d2
--- /dev/null
+++ b/rust-runtime/inlineable/src/endpoint_lib/host.rs
@@ -0,0 +1,76 @@
+/*
+ *  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *  SPDX-License-Identifier: Apache-2.0
+ */
+
+use crate::endpoint_lib::diagnostic::DiagnosticCollector;
+
+pub(crate) fn is_valid_host_label(
+    label: &str,
+    allow_dots: bool,
+    e: &mut DiagnosticCollector,
+) -> bool {
+    if allow_dots {
+        for part in label.split('.') {
+            if !is_valid_host_label(part, false, e) {
+                return false;
+            }
+        }
+        true
+    } else {
+        if label.is_empty() || label.len() > 63 {
+            e.report_error("host was too short or too long");
+            return false;
+        }
+        label.chars().enumerate().all(|(idx, ch)| match (ch, idx) {
+            ('-', 0) => {
+                e.report_error("cannot start with `-`");
+                false
+            }
+            _ => ch.is_alphanumeric() || ch == '-',
+        })
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use proptest::proptest;
+
+    fn is_valid_host_label(label: &str, allow_dots: bool) -> bool {
+        super::is_valid_host_label(label, allow_dots, &mut DiagnosticCollector::new())
+    }
+
+    #[test]
+    fn basic_cases() {
+        assert_eq!(is_valid_host_label("", false), false);
+        assert_eq!(is_valid_host_label("", true), false);
+        assert_eq!(is_valid_host_label(".", true), false);
+        assert_eq!(is_valid_host_label("a.b", true), true);
+        assert_eq!(is_valid_host_label("a.b", false), false);
+        assert_eq!(is_valid_host_label("a.b.", true), false);
+        assert_eq!(is_valid_host_label("a.b.c", true), true);
+        assert_eq!(is_valid_host_label("a_b", true), false);
+        assert_eq!(is_valid_host_label(&"a".repeat(64), false), false);
+        assert_eq!(
+            is_valid_host_label(&format!("{}.{}", "a".repeat(63), "a".repeat(63)), true),
+            true
+        );
+    }
+
+    #[test]
+    fn start_bounds() {
+        assert_eq!(is_valid_host_label("-foo", false), false);
+        assert_eq!(is_valid_host_label("-foo", true), false);
+        assert_eq!(is_valid_host_label(".foo", true), false);
+        assert_eq!(is_valid_host_label("a-b.foo", true), true);
+    }
+
+    use crate::endpoint_lib::diagnostic::DiagnosticCollector;
+    use proptest::prelude::*;
+    proptest! {
+        #[test]
+        fn no_panics(s in any::<String>(), dots in any::<bool>()) {
+            is_valid_host_label(&s, dots);
+        }
+    }
+}
diff --git a/rust-runtime/inlineable/src/endpoint_lib/parse_url.rs b/rust-runtime/inlineable/src/endpoint_lib/parse_url.rs
new file mode 100644
index 0000000000..0354ba335c
--- /dev/null
+++ b/rust-runtime/inlineable/src/endpoint_lib/parse_url.rs
@@ -0,0 +1,109 @@
+/*
+ *  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *  SPDX-License-Identifier: Apache-2.0
+ */
+
+use crate::endpoint_lib::diagnostic::DiagnosticCollector;
+use http::Uri;
+use std::error::Error;
+use url::{Host, Url as ParsedUrl};
+
+#[derive(PartialEq, Debug)]
+pub(crate) struct Url<'a> {
+    uri: Uri,
+    url: ParsedUrl,
+    raw: &'a str,
+}
+
+impl<'a> Url<'a> {
+    pub(crate) fn is_ip(&self) -> bool {
+        matches!(self.url.host(), Some(Host::Ipv4(_) | Host::Ipv6(_)))
+    }
+    pub(crate) fn scheme(&self) -> &str {
+        self.url.scheme()
+    }
+
+    pub(crate) fn authority(&self) -> &str {
+        self.uri.authority().unwrap().as_str()
+    }
+
+    pub(crate) fn normalized_path(&self) -> &str {
+        match self.uri.path() {
+            path if !path.is_empty() => path,
+            _ => "/",
+        }
+    }
+
+    pub(crate) fn path(&self) -> &str {
+        if self.uri.path() == "/" && !self.raw.ends_with('/') {
+            ""
+        } else {
+            self.uri.path()
+        }
+    }
+}
+
+pub(crate) fn parse_url<'a, 'b>(url: &'a str, e: &'b mut DiagnosticCollector) -> Option<Url<'a>> {
+    let raw = url;
+    let uri: Uri = e.capture(url.parse())?;
+    let url: ParsedUrl = e.capture(url.parse())?;
+    if let Some(query) = uri.query() {
+        e.report_error(format!(
+            "URL cannot have a query component (found {})",
+            query
+        ));
+        return None;
+    }
+    if !["http", "https"].contains(&url.scheme()) {
+        e.report_error(format!(
+            "URL scheme must be HTTP or HTTPS (found {})",
+            url.scheme()
+        ));
+        return None;
+    }
+    Some(Url { url, uri, raw })
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::endpoint_lib::diagnostic::DiagnosticCollector;
+
+    #[test]
+    fn parse_simple_url() {
+        let url = "https://control.vpce-1a2b3c4d-5e6f.s3.us-west-2.vpce.amazonaws.com";
+        let url = parse_url(url, &mut DiagnosticCollector::new()).expect("valid url");
+        assert_eq!(url.path(), "");
+        assert_eq!(url.normalized_path(), "/");
+        assert_eq!(url.is_ip(), false);
+        assert_eq!(url.scheme(), "https");
+        assert_eq!(
+            url.authority(),
+            "control.vpce-1a2b3c4d-5e6f.s3.us-west-2.vpce.amazonaws.com"
+        );
+    }
+
+    #[test]
+    fn schemes_are_normalized() {
+        let url = "HTTPS://control.vpce-1a2b3c4d-5e6f.s3.us-west-2.vpce.amazonaws.com";
+        let url = parse_url(url, &mut DiagnosticCollector::new()).expect("valid url");
+        assert_eq!(url.scheme(), "https");
+    }
+
+    #[test]
+    fn parse_url_with_port() {
+        let url = "http://localhost:8000/path";
+        let url = parse_url(url, &mut DiagnosticCollector::new()).expect("valid url");
+        assert_eq!(url.path(), "/path");
+        assert_eq!(url.normalized_path(), "/path");
+        assert_eq!(url.is_ip(), false);
+        assert_eq!(url.scheme(), "http");
+        assert_eq!(url.authority(), "localhost:8000");
+    }
+
+    #[test]
+    fn only_http_https_supported() {
+        let url = "wss://localhost:8443/path";
+        assert_eq!(parse_url(url, &mut DiagnosticCollector::new()), None);
+    }
+}
diff --git a/rust-runtime/inlineable/src/endpoint_lib/substring.rs b/rust-runtime/inlineable/src/endpoint_lib/substring.rs
new file mode 100644
index 0000000000..3e55b08009
--- /dev/null
+++ b/rust-runtime/inlineable/src/endpoint_lib/substring.rs
@@ -0,0 +1,113 @@
+/*
+ *  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *  SPDX-License-Identifier: Apache-2.0
+ */
+
+use crate::endpoint_lib::diagnostic::DiagnosticCollector;
+
+/// substring of `input`
+///
+/// > Note: this function only operates on ASCII input. If the input contains non-ASCII characters,
+/// > `None` will be returned.
+///
+/// - When `reverse` is false, indexes are evaluated from the beginning of the string
+/// - When `reverse` is true, indexes are evaluated from the end of the string (however, the result
+///   will still be "forwards" and `start` MUST be less than `end`.
+pub(crate) fn substring<'a, 'b>(
+    input: &'a str,
+    start: usize,
+    stop: usize,
+    reverse: bool,
+    e: &'b mut DiagnosticCollector,
+) -> Option<&'a str> {
+    if start >= stop {
+        e.capture(Err("start > stop"))?;
+    }
+    if !input.is_ascii() {
+        e.capture(Err("the input to substring was not ascii"))?;
+    }
+    if input.len() < stop {
+        e.capture(Err("the input was too short"))?;
+    }
+    let (effective_start, effective_stop) = if !reverse {
+        (start, stop)
+    } else {
+        (input.len() - stop, input.len() - start)
+    };
+    Some(&input[effective_start..effective_stop])
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use proptest::proptest;
+
+    #[test]
+    fn substring_forwards() {
+        assert_eq!(
+            substring("hello", 0, 2, false, &mut DiagnosticCollector::new()),
+            Some("he")
+        );
+        assert_eq!(
+            substring("hello", 0, 0, false, &mut DiagnosticCollector::new()),
+            None
+        );
+        assert_eq!(
+            substring("hello", 0, 5, false, &mut DiagnosticCollector::new()),
+            Some("hello")
+        );
+        assert_eq!(
+            substring("hello", 0, 6, false, &mut DiagnosticCollector::new()),
+            None
+        );
+    }
+    fn substring_backwards() {
+        assert_eq!(
+            substring("hello", 0, 2, true, &mut DiagnosticCollector::new()),
+            Some("lo")
+        );
+        assert_eq!(
+            substring("hello", 0, 0, true, &mut DiagnosticCollector::new()),
+            None
+        );
+        assert_eq!(
+            substring("hello", 0, 5, true, &mut DiagnosticCollector::new()),
+            Some("hello")
+        )
+    }
+
+    // substring doesn't support unicode, it always returns none
+    #[test]
+    fn substring_unicode() {
+        let mut collector = DiagnosticCollector::new();
+        assert_eq!(substring("a🐱b", 0, 2, false, &mut collector), None);
+        assert_eq!(
+            format!(
+                "{}",
+                collector
+                    .take_last_error()
+                    .expect("last error should be set")
+            ),
+            "the input to substring was not ascii"
+        );
+    }
+
+    use proptest::prelude::*;
+    proptest! {
+        #[test]
+        fn substring_no_panics(s in any::<String>(), start in 0..100usize, stop in 0..100usize, reverse in proptest::bool::ANY) {
+            substring(&s, start, stop, reverse, &mut DiagnosticCollector::new());
+        }
+
+        #[test]
+        fn substring_correct_length(s in r#"[\x00-\xFF]*"#, start in 0..10usize, stop in 0..10usize, reverse in proptest::bool::ANY) {
+            prop_assume!(start < s.len());
+            prop_assume!(stop < s.len());
+            prop_assume!(start < stop);
+            if let Some(result) = substring(&s, start, stop, reverse, &mut DiagnosticCollector::new()) {
+                assert_eq!(result.len(), stop - start);
+            }
+
+        }
+    }
+}
diff --git a/rust-runtime/inlineable/src/lib.rs b/rust-runtime/inlineable/src/lib.rs
index a26d779f30..3cbcd5e5ff 100644
--- a/rust-runtime/inlineable/src/lib.rs
+++ b/rust-runtime/inlineable/src/lib.rs
@@ -16,6 +16,8 @@ mod rest_xml_wrapped_errors;
 #[allow(unused)]
 mod server_operation_handler_trait;
 
+#[allow(unused)]
+mod endpoint_lib;
 // This test is outside of uuid.rs to enable copying the entirety of uuid.rs into the SDK without
 // requiring a proptest dependency
 #[cfg(test)]