Use url::form_urlencoded for OAuth form body encoding

Richard Feldman created

Replace hand-rolled format! string interpolation with
url::form_urlencoded::Serializer in exchange_code and refresh_token.
The previous code didn't percent-encode the code, verifier, or
refresh_token values, which would break if they contained &, =, or +.

Change summary

crates/language_models/Cargo.toml                        |  1 
crates/language_models/src/provider/openai_subscribed.rs | 19 ++++++---
2 files changed, 14 insertions(+), 6 deletions(-)

Detailed changes

crates/language_models/Cargo.toml 🔗

@@ -65,6 +65,7 @@ tiktoken-rs.workspace = true
 tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
 ui.workspace = true
 ui_input.workspace = true
+url.workspace = true
 util.workspace = true
 vercel = { workspace = true, features = ["schemars"] }
 x_ai = { workspace = true, features = ["schemars"] }

crates/language_models/src/provider/openai_subscribed.rs 🔗

@@ -19,6 +19,7 @@ use smol::io::{AsyncReadExt as _, AsyncWriteExt as _};
 use std::sync::Arc;
 use std::time::{Duration, SystemTime, UNIX_EPOCH};
 use ui::{ConfiguredApiCard, prelude::*};
+use url::form_urlencoded;
 use util::ResultExt as _;
 
 use crate::provider::open_ai::{OpenAiResponseEventMapper, into_open_ai_response};
@@ -749,10 +750,13 @@ async fn exchange_code(
     code: &str,
     verifier: &str,
 ) -> Result<TokenResponse> {
-    let body = format!(
-        "grant_type=authorization_code&client_id={CLIENT_ID}&code={code}&redirect_uri={encoded_redirect}&code_verifier={verifier}",
-        encoded_redirect = percent_encode(REDIRECT_URI),
-    );
+    let body = form_urlencoded::Serializer::new(String::new())
+        .append_pair("grant_type", "authorization_code")
+        .append_pair("client_id", CLIENT_ID)
+        .append_pair("code", code)
+        .append_pair("redirect_uri", REDIRECT_URI)
+        .append_pair("code_verifier", verifier)
+        .finish();
 
     let request = HttpRequest::builder()
         .method(Method::POST)
@@ -778,8 +782,11 @@ async fn refresh_token(
     client: &Arc<dyn HttpClient>,
     refresh_token: &str,
 ) -> Result<CodexCredentials> {
-    let body =
-        format!("grant_type=refresh_token&client_id={CLIENT_ID}&refresh_token={refresh_token}");
+    let body = form_urlencoded::Serializer::new(String::new())
+        .append_pair("grant_type", "refresh_token")
+        .append_pair("client_id", CLIENT_ID)
+        .append_pair("refresh_token", refresh_token)
+        .finish();
 
     let request = HttpRequest::builder()
         .method(Method::POST)