Skip to content

Commit

Permalink
server: Don't require trailing slash at the end of request paths (#1270)
Browse files Browse the repository at this point in the history
Closes #1254.
  • Loading branch information
svix-jplatte authored Mar 13, 2024
2 parents 08142f2 + 410a927 commit 2061286
Show file tree
Hide file tree
Showing 12 changed files with 111 additions and 81 deletions.
60 changes: 30 additions & 30 deletions server/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2040,7 +2040,7 @@
},
"openapi": "3.0.2",
"paths": {
"/api/v1/app/": {
"/api/v1/app": {
"get": {
"description": "List of all the organization's applications.",
"operationId": "v1.application.list",
Expand Down Expand Up @@ -2281,7 +2281,7 @@
]
}
},
"/api/v1/app/{app_id}/": {
"/api/v1/app/{app_id}": {
"delete": {
"description": "Delete an application.",
"operationId": "v1.application.delete",
Expand Down Expand Up @@ -2641,7 +2641,7 @@
]
}
},
"/api/v1/app/{app_id}/attempt/endpoint/{endpoint_id}/": {
"/api/v1/app/{app_id}/attempt/endpoint/{endpoint_id}": {
"get": {
"description": "List attempts by endpoint id",
"operationId": "v1.message-attempt.list-by-endpoint",
Expand Down Expand Up @@ -2864,7 +2864,7 @@
]
}
},
"/api/v1/app/{app_id}/attempt/msg/{msg_id}/": {
"/api/v1/app/{app_id}/attempt/msg/{msg_id}": {
"get": {
"description": "List attempts by message id",
"operationId": "v1.message-attempt.list-by-msg",
Expand Down Expand Up @@ -3102,7 +3102,7 @@
]
}
},
"/api/v1/app/{app_id}/endpoint/": {
"/api/v1/app/{app_id}/endpoint": {
"get": {
"description": "List the application's endpoints.",
"operationId": "v1.endpoint.list",
Expand Down Expand Up @@ -3348,7 +3348,7 @@
]
}
},
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/": {
"/api/v1/app/{app_id}/endpoint/{endpoint_id}": {
"delete": {
"description": "Delete an endpoint.",
"operationId": "v1.endpoint.delete",
Expand Down Expand Up @@ -3805,7 +3805,7 @@
]
}
},
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/headers/": {
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/headers": {
"get": {
"description": "Get the additional headers to be sent with the webhook",
"operationId": "v1.endpoint.get-headers",
Expand Down Expand Up @@ -4137,7 +4137,7 @@
]
}
},
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/msg/": {
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/msg": {
"get": {
"description": "List messages for a particular endpoint. Additionally includes metadata about the latest message attempt.\n\nThe `before` parameter lets you filter all items created before a certain date and is ignored if an iterator is passed.",
"operationId": "v1.message-attempt.list-attempted-messages",
Expand Down Expand Up @@ -4349,7 +4349,7 @@
]
}
},
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/recover/": {
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/recover": {
"post": {
"description": "Resend all failed messages since a given time.",
"operationId": "v1.endpoint.recover",
Expand Down Expand Up @@ -4471,7 +4471,7 @@
]
}
},
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/secret/": {
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/secret": {
"get": {
"description": "Get the endpoint's signing secret.\n\nThis is used to verify the authenticity of the webhook.\nFor more information please refer to [the consuming webhooks docs](https://docs.svix.com/consuming-webhooks/).",
"operationId": "v1.endpoint.get-secret",
Expand Down Expand Up @@ -4581,7 +4581,7 @@
]
}
},
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/secret/rotate/": {
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/secret/rotate": {
"post": {
"description": "Rotates the endpoint's signing secret. The previous secret will be valid for the next 24 hours.",
"operationId": "v1.endpoint.rotate-secret",
Expand Down Expand Up @@ -4703,7 +4703,7 @@
]
}
},
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/send-example/": {
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/send-example": {
"post": {
"description": "Send an example message for an event",
"operationId": "v1.endpoint.send-example",
Expand Down Expand Up @@ -4832,7 +4832,7 @@
]
}
},
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/stats/": {
"/api/v1/app/{app_id}/endpoint/{endpoint_id}/stats": {
"get": {
"description": "Get basic statistics for the endpoint.",
"operationId": "v1.endpoint.get-stats",
Expand Down Expand Up @@ -4962,7 +4962,7 @@
]
}
},
"/api/v1/app/{app_id}/msg/": {
"/api/v1/app/{app_id}/msg": {
"get": {
"description": "List all of the application's messages.\n\nThe `before` parameter lets you filter all items created before a certain date and is ignored if an iterator is passed.\nThe `after` parameter lets you filter all items created after a certain date and is ignored if an iterator is passed.\n`before` and `after` cannot be used simultaneously.",
"operationId": "v1.message.list",
Expand Down Expand Up @@ -5249,7 +5249,7 @@
]
}
},
"/api/v1/app/{app_id}/msg/{msg_id}/": {
"/api/v1/app/{app_id}/msg/{msg_id}": {
"get": {
"description": "Get a message by its ID or eventID.",
"operationId": "v1.message.get",
Expand Down Expand Up @@ -5370,7 +5370,7 @@
]
}
},
"/api/v1/app/{app_id}/msg/{msg_id}/attempt/": {
"/api/v1/app/{app_id}/msg/{msg_id}/attempt": {
"get": {
"description": "Deprecated: Please use \"List Attempts by Endpoint\" and \"List Attempts by Msg\" instead.\n\n`msg_id`: Use a message id or a message `eventId`",
"operationId": "v1.message-attempt.list-by-msg-deprecated",
Expand Down Expand Up @@ -5586,7 +5586,7 @@
]
}
},
"/api/v1/app/{app_id}/msg/{msg_id}/attempt/{attempt_id}/": {
"/api/v1/app/{app_id}/msg/{msg_id}/attempt/{attempt_id}": {
"get": {
"description": "`msg_id`: Use a message id or a message `eventId`",
"operationId": "v1.message-attempt.get",
Expand Down Expand Up @@ -5706,7 +5706,7 @@
]
}
},
"/api/v1/app/{app_id}/msg/{msg_id}/attempt/{attempt_id}/content/": {
"/api/v1/app/{app_id}/msg/{msg_id}/attempt/{attempt_id}/content": {
"delete": {
"description": "Deletes the given attempt's response body. Useful when an endpoint accidentally returned sensitive content.",
"operationId": "v1.message-attempt.expunge-content",
Expand Down Expand Up @@ -5816,7 +5816,7 @@
]
}
},
"/api/v1/app/{app_id}/msg/{msg_id}/content/": {
"/api/v1/app/{app_id}/msg/{msg_id}/content": {
"delete": {
"description": "Delete the given message's payload. Useful in cases when a message was accidentally sent with sensitive content.\n\nThe message can't be replayed or resent once its payload has been deleted or expired.",
"operationId": "v1.message.expunge-content",
Expand Down Expand Up @@ -5916,7 +5916,7 @@
]
}
},
"/api/v1/app/{app_id}/msg/{msg_id}/endpoint/": {
"/api/v1/app/{app_id}/msg/{msg_id}/endpoint": {
"get": {
"description": "`msg_id`: Use a message id or a message `eventId`",
"operationId": "v1.message-attempt.list-attempted-destinations",
Expand Down Expand Up @@ -6050,7 +6050,7 @@
]
}
},
"/api/v1/app/{app_id}/msg/{msg_id}/endpoint/{endpoint_id}/attempt/": {
"/api/v1/app/{app_id}/msg/{msg_id}/endpoint/{endpoint_id}/attempt": {
"get": {
"description": "DEPRECATED: please use list_attempts with endpoint_id as a query parameter instead.\n\nList the message attempts for a particular endpoint.\n\nReturning the endpoint.\n\nThe `before` parameter lets you filter all items created before a certain date and is ignored if an iterator is passed.",
"operationId": "v1.message-attempt.list-by-endpoint-deprecated",
Expand Down Expand Up @@ -6264,7 +6264,7 @@
]
}
},
"/api/v1/app/{app_id}/msg/{msg_id}/endpoint/{endpoint_id}/resend/": {
"/api/v1/app/{app_id}/msg/{msg_id}/endpoint/{endpoint_id}/resend": {
"post": {
"description": "Resend a message to the specified endpoint.",
"operationId": "v1.message-attempt.resend",
Expand Down Expand Up @@ -6389,7 +6389,7 @@
]
}
},
"/api/v1/auth/app-portal-access/{app_id}/": {
"/api/v1/auth/app-portal-access/{app_id}": {
"post": {
"description": "Use this function to get magic links (and authentication codes) for connecting your users to the Consumer Application Portal.",
"operationId": "v1.authentication.app-portal-access",
Expand Down Expand Up @@ -6505,7 +6505,7 @@
]
}
},
"/api/v1/auth/dashboard-access/{app_id}/": {
"/api/v1/auth/dashboard-access/{app_id}": {
"post": {
"description": "DEPRECATED: Please use `app-portal-access` instead.\n\nUse this function to get magic links (and authentication codes) for connecting your users to the Consumer Application Portal.",
"operationId": "v1.authentication.dashboard-access",
Expand Down Expand Up @@ -6611,7 +6611,7 @@
]
}
},
"/api/v1/auth/logout/": {
"/api/v1/auth/logout": {
"post": {
"description": "\nLogout an app token.\n\nTrying to log out other tokens will fail.\n",
"operationId": "logout_api_v1_auth_logout__post",
Expand All @@ -6637,7 +6637,7 @@
]
}
},
"/api/v1/event-type/": {
"/api/v1/event-type": {
"get": {
"description": "Return the list of event types.",
"operationId": "v1.event-type.list",
Expand Down Expand Up @@ -6881,7 +6881,7 @@
]
}
},
"/api/v1/event-type/schema/generate-example/": {
"/api/v1/event-type/schema/generate-example": {
"post": {
"description": "Generates a fake example from the given JSONSchema",
"parameters": [
Expand All @@ -6905,7 +6905,7 @@
]
}
},
"/api/v1/event-type/{event_type_name}/": {
"/api/v1/event-type/{event_type_name}": {
"delete": {
"description": "Archive an event type.\n\nEndpoints already configured to filter on an event type will continue to do so after archival.\nHowever, new messages can not be sent with it and endpoints can not filter on it.\nAn event type can be unarchived with the\n[create operation](#operation/create_event_type_api_v1_event_type__post).",
"operationId": "v1.event-type.delete",
Expand Down Expand Up @@ -7306,7 +7306,7 @@
]
}
},
"/api/v1/health/": {
"/api/v1/health": {
"get": {
"description": "Verify the API server is up and running.",
"operationId": "v1.health.get",
Expand Down Expand Up @@ -7451,7 +7451,7 @@
]
}
},
"/api/v1/health/ping/": {
"/api/v1/health/ping": {
"get": {
"responses": {
"204": {
Expand Down
2 changes: 1 addition & 1 deletion server/svix-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ hyper-openssl = "0.9.2"
openssl = "0.10.60"
tokio = { version = "1.24.2", features = ["full"] }
tower = "0.4.11"
tower-http = { version = "0.4.0", features = ["trace", "cors", "request-id"] }
tower-http = { version = "0.4.4", features = ["trace", "cors", "normalize-path", "request-id"] }
serde = { version = "1.0.184", features = ["derive"] }
serde_json = { version = "1.0.74", features = ["arbitrary_precision", "raw_value"] }
serde_path_to_error = "0.1.7"
Expand Down
43 changes: 23 additions & 20 deletions server/svix-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ use std::{
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use tower::ServiceBuilder;
use tower_http::cors::{AllowHeaders, Any, CorsLayer};
use tower::layer::layer_fn;
use tower_http::{
cors::{AllowHeaders, Any, CorsLayer},
normalize_path::NormalizePath,
};
use tracing_subscriber::layer::SubscriberExt as _;

use crate::{
Expand Down Expand Up @@ -149,25 +152,25 @@ pub async fn run_with_prefix(

let openapi = openapi::postprocess_spec(openapi);
let docs_router = docs::router(openapi);
let app = app
.merge(docs_router)
.layer(
ServiceBuilder::new().layer_fn(move |service| IdempotencyService {
cache: svc_cache.clone(),
service,
}),
)
.layer(
CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(AllowHeaders::mirror_request())
.max_age(Duration::from_secs(600)),
);
let app = app.merge(docs_router).layer((
layer_fn(move |service| IdempotencyService {
cache: svc_cache.clone(),
service,
}),
CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(AllowHeaders::mirror_request())
.max_age(Duration::from_secs(600)),
));
let svc = tower::make::Shared::new(
// It is important that this service wraps the router instead of being
// applied via `Router::layer`, as it would run after routing then.
NormalizePath::trim_trailing_slash(app),
);

let with_api = cfg.api_enabled;
let with_worker = cfg.worker_enabled;

let listen_address = cfg.listen_address;

let (server, worker_loop, expired_message_cleaner_loop) = tokio::join!(
Expand All @@ -177,13 +180,13 @@ pub async fn run_with_prefix(
tracing::debug!("API: Listening on {}", l.local_addr().unwrap());
axum::Server::from_tcp(l)
.expect("Error starting http server")
.serve(app.into_make_service())
.serve(svc)
.with_graceful_shutdown(graceful_shutdown_handler())
.await
} else {
tracing::debug!("API: Listening on {}", listen_address);
axum::Server::bind(&listen_address)
.serve(app.into_make_service())
.serve(svc)
.with_graceful_shutdown(graceful_shutdown_handler())
.await
}
Expand Down
4 changes: 2 additions & 2 deletions server/svix-server/src/v1/endpoints/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,13 +389,13 @@ pub fn router() -> ApiRouter<AppState> {
let tag = openapi_tag("Application");
ApiRouter::new()
.api_route_with(
"/app/",
"/app",
post_with(create_application, create_application_operation)
.get_with(list_applications, list_applications_operation),
&tag,
)
.api_route_with(
"/app/:app_id/",
"/app/:app_id",
get_with(get_application, get_application_operation)
.put_with(update_application, update_application_operation)
.patch_with(patch_application, patch_application_operation)
Expand Down
Loading

0 comments on commit 2061286

Please sign in to comment.