Fixing issue #830 (#837)

* Fixing issue #830

* fix: fetch template metadata without auth headers

Railway template lookup endpoints reject authenticated requests, so use a public GraphQL client for template detail and template search requests. Add a regression test so add --database keeps working for logged-in users.

* chore: drop unrelated auth changes

Keep the postgres auth fix focused by removing the stray npm lockfile and the unrelated API token handling change from this PR.
This commit is contained in:
Brandon 2026-04-15 18:40:07 -07:00 committed by GitHub
parent 8ab2d5a881
commit 991d8f0b15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 123 additions and 12 deletions

View file

@ -20,6 +20,16 @@ use graphql_client::Response as GraphQLResponse;
pub struct GQLClient;
impl GQLClient {
pub fn new_public() -> Result<Client, RailwayError> {
let mut headers = HeaderMap::new();
headers.insert(
"x-source",
HeaderValue::from_static(consts::get_user_agent()),
);
Ok(Self::build_client(headers))
}
pub fn new_authorized(configs: &Configs) -> Result<Client, RailwayError> {
let mut headers = HeaderMap::new();
if let Some(token) = &Configs::get_railway_token() {
@ -36,14 +46,17 @@ impl GQLClient {
"x-source",
HeaderValue::from_static(consts::get_user_agent()),
);
let client = Client::builder()
Ok(Self::build_client(headers))
}
fn build_client(headers: HeaderMap) -> Client {
Client::builder()
.danger_accept_invalid_certs(matches!(Configs::get_environment_id(), Environment::Dev))
.user_agent(consts::get_user_agent())
.default_headers(headers)
.timeout(Duration::from_secs(30))
.build()
.unwrap();
Ok(client)
.unwrap()
}
}
@ -211,3 +224,93 @@ pub async fn post_graphql_skip_none<Q: GraphQLQuery, U: reqwest::IntoUrl>(
Err(RailwayError::MissingResponseData)
}
}
#[cfg(test)]
mod tests {
use std::{
io::{BufRead, BufReader, Read, Write},
net::TcpListener,
thread,
};
use super::*;
use crate::gql::queries;
fn spawn_graphql_server(
response_for_request: impl FnOnce(String) -> String + Send + 'static,
) -> String {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut request = String::new();
let mut content_length = 0usize;
loop {
let mut line = String::new();
reader.read_line(&mut line).unwrap();
request.push_str(&line);
if let Some(value) = line.strip_prefix("Content-Length:") {
content_length = value.trim().parse().unwrap();
}
if line == "\r\n" {
break;
}
}
let mut body = vec![0; content_length];
reader.read_exact(&mut body).unwrap();
request.push_str(std::str::from_utf8(&body).unwrap());
let response_body = response_for_request(request);
let response = format!(
"HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
response_body.len(),
response_body
);
stream.write_all(response.as_bytes()).unwrap();
});
format!("http://{addr}")
}
#[tokio::test]
async fn public_client_can_query_templates_without_auth_headers() {
let server_url = spawn_graphql_server(|request| {
assert!(
!request.to_ascii_lowercase().contains("authorization:"),
"public template lookup should not send auth headers"
);
serde_json::json!({
"data": {
"template": {
"id": "template-id",
"name": "PostgreSQL",
"serializedConfig": null
}
}
})
.to_string()
});
let client = GQLClient::new_public().unwrap();
let response = post_graphql::<queries::TemplateDetail, _>(
&client,
server_url,
queries::template_detail::Variables {
code: "postgres".to_string(),
},
)
.await
.unwrap();
assert_eq!(response.template.id, "template-id");
assert_eq!(response.template.name, "PostgreSQL");
assert_eq!(response.template.serialized_config, None);
}
}

View file

@ -111,8 +111,9 @@ pub async fn fetch_and_create(
if verbose {
println!("fetching details for template")
}
let public_client = GQLClient::new_public()?;
let details = post_graphql::<queries::TemplateDetail, _>(
client,
&public_client,
configs.get_backboard(),
queries::template_detail::Variables {
code: template.clone(),

View file

@ -3,7 +3,7 @@ use std::collections::BTreeMap;
use rmcp::{ErrorData as McpError, model::*};
use crate::{
client::post_graphql,
client::{GQLClient, post_graphql},
controllers::config::environment::fetch_environment_config,
gql::{mutations, queries},
};
@ -142,13 +142,16 @@ impl RailwayMcp {
let ctx = self
.resolve_context(params.project_id, params.environment_id)
.await?;
let public_client = GQLClient::new_public().map_err(|e| {
McpError::internal_error(format!("Failed to create template client: {e}"), None)
})?;
let template_vars = queries::template_detail::Variables {
code: params.template_code,
};
let template_resp = post_graphql::<queries::TemplateDetail, _>(
&self.client,
&public_client,
self.configs.get_backboard(),
template_vars,
)
@ -188,18 +191,22 @@ impl RailwayMcp {
&self,
params: SearchTemplatesParams,
) -> Result<CallToolResult, McpError> {
let public_client = GQLClient::new_public().map_err(|e| {
McpError::internal_error(format!("Failed to create template client: {e}"), None)
})?;
let vars = queries::templates::Variables {
verified: None,
recommended: None,
first: Some(200),
};
let resp =
post_graphql::<queries::Templates, _>(&self.client, self.configs.get_backboard(), vars)
.await
.map_err(|e| {
McpError::internal_error(format!("Failed to fetch templates: {e}"), None)
})?;
let resp = post_graphql::<queries::Templates, _>(
&public_client,
self.configs.get_backboard(),
vars,
)
.await
.map_err(|e| McpError::internal_error(format!("Failed to fetch templates: {e}"), None))?;
let query_lower = params.query.to_lowercase();
let results: Vec<_> = resp