@@ -808,6 +808,240 @@ TEST(http_client, set_proxy_with_auth) {
808808 EXPECT_EQ (client->get_proxy ()->port (), 8080 );
809809}
810810
811+ // Verify that Operation::set_proxy/clear_proxy manage operation-level proxy state
812+ TEST (http_client, operation_set_clear_proxy) {
813+ // Client without proxy
814+ auto client = new_http_client ();
815+ DEFER (delete client);
816+
817+ auto op = client->new_operation (Verb::GET , " http://example.com/path" );
818+ DEFER (client->destroy_operation (op));
819+
820+ EXPECT_TRUE (op->get_proxy ()->empty ());
821+ EXPECT_FALSE (op->enable_proxy );
822+
823+ // Operation::set_proxy should enable proxy at operation level
824+ op->set_proxy (" http://proxy1:8080" );
825+ EXPECT_FALSE (op->get_proxy ()->empty ());
826+ EXPECT_EQ (op->get_proxy ()->host (), " proxy1" );
827+ EXPECT_EQ (op->get_proxy ()->port (), 8080 );
828+ EXPECT_TRUE (op->enable_proxy );
829+
830+ // clear_proxy should clear operation-level proxy but keep enable_proxy
831+ op->clear_proxy ();
832+ EXPECT_TRUE (op->get_proxy ()->empty ());
833+ EXPECT_TRUE (op->enable_proxy );
834+ }
835+
836+ // Verify set_proxy/clear_proxy keep request line unchanged when enable_proxy is already true
837+ TEST (http_client, operation_set_clear_proxy_no_rebuild) {
838+ auto client = new_http_client ();
839+ DEFER (delete client);
840+ client->set_proxy (" http://default-proxy:8080" );
841+
842+ auto op = client->new_operation (Verb::GET , " http://example.com/path" );
843+ DEFER (client->destroy_operation (op));
844+
845+ // Client-level proxy is set, so enable_proxy is already true
846+ EXPECT_TRUE (op->enable_proxy );
847+ auto target_before = std::string (op->req .target ());
848+
849+ // Set operation-level proxy; should not rebuild request line
850+ op->set_proxy (" http://op-proxy:9090" );
851+ EXPECT_EQ (op->req .target (), target_before);
852+ EXPECT_FALSE (op->get_proxy ()->empty ());
853+ EXPECT_EQ (op->get_proxy ()->host (), " op-proxy" );
854+ EXPECT_EQ (op->get_proxy ()->port (), 9090 );
855+
856+ // Clear operation-level proxy; request line still unchanged
857+ op->clear_proxy ();
858+ EXPECT_TRUE (op->get_proxy ()->empty ());
859+ EXPECT_EQ (op->req .target (), target_before);
860+ EXPECT_TRUE (op->enable_proxy );
861+ }
862+
863+ // Verify set_proxy/clear_proxy rebuild request line between relative and absolute URI
864+ TEST (http_client, operation_set_proxy_rebuilds_request_line) {
865+ auto client = new_http_client ();
866+ DEFER (delete client);
867+ // No client-level proxy
868+
869+ auto op = client->new_operation (Verb::GET , " http://example.com/path?query=1" );
870+ DEFER (client->destroy_operation (op));
871+
872+ // Before set_proxy: request line should be relative path
873+ EXPECT_FALSE (op->enable_proxy );
874+ EXPECT_EQ (op->req .target (), " /path?query=1" );
875+
876+ // After set_proxy: request line should be absolute URI
877+ op->set_proxy (" http://myproxy:3128" );
878+ EXPECT_TRUE (op->enable_proxy );
879+ EXPECT_EQ (op->req .target (), " http://example.com/path?query=1" );
880+
881+ // After clear_proxy: enable_proxy still true (request line unchanged),
882+ // but no proxy is configured
883+ op->clear_proxy ();
884+ EXPECT_TRUE (op->enable_proxy );
885+ EXPECT_EQ (op->req .target (), " http://example.com/path?query=1" );
886+ EXPECT_TRUE (op->get_proxy ()->empty ());
887+ }
888+
889+ // E2E test: different operations use different proxies through the same client
890+ namespace {
891+ int op_proxy_director (void * src_, Request& src, Request& dst) {
892+ auto source_server = (ISocketServer*)src_;
893+ // src.target() may be absolute URI (via proxy) or relative path (direct)
894+ // Parse it to extract the path portion for forwarding
895+ URL src_url (src.target ());
896+ auto url = to_url (source_server, src_url.path ());
897+ dst.reset (src.verb (), url);
898+ for (auto kv = src.headers .begin (); kv != src.headers .end (); kv++) {
899+ if (kv.first () != " Host" )
900+ dst.headers .insert (kv.first (), kv.second (), 1 );
901+ }
902+ return 0 ;
903+ }
904+ int op_proxy_modifier (void * ctx, Response& src, Response& dst) {
905+ dst.set_result (src.status_code ());
906+ for (auto kv : src.headers ) {
907+ dst.headers .insert (kv.first , kv.second );
908+ }
909+ // Add X-Via header to identify which proxy handled the request
910+ if (ctx) {
911+ dst.headers .insert (" X-Via" , static_cast <const char *>(ctx));
912+ }
913+ return 0 ;
914+ }
915+ int op_proxy_echo_handler (void *, Request& req, Response& resp, std::string_view) {
916+ // Verify the request path was correctly forwarded
917+ EXPECT_EQ (req.target (), " /echo" );
918+ // Echo: read the request body and send it back as-is
919+ auto body_len = req.headers .content_length ();
920+ resp.set_result (200 );
921+ resp.headers .content_length (body_len);
922+ char buf[4096 ];
923+ ssize_t n;
924+ while ((n = req.read (buf, sizeof (buf))) > 0 ) {
925+ resp.write (buf, n);
926+ }
927+ return 0 ;
928+ }
929+ } // anonymous namespace
930+
931+ TEST (http_client, operation_level_proxy_e2e) {
932+ // --------start source server ------------
933+ auto source_server = new_tcp_socket_server ();
934+ source_server->timeout (1000UL *1000 );
935+ source_server->bind_v4localhost ();
936+ source_server->listen ();
937+ DEFER (delete source_server);
938+ auto source_http_server = new_http_server ();
939+ DEFER (delete source_http_server);
940+ source_http_server->add_handler ({nullptr , &op_proxy_echo_handler});
941+ source_server->set_handler (source_http_server->get_connection_handler ());
942+ source_server->start_loop ();
943+
944+ photon::thread_sleep (1 );
945+
946+ // --------start proxy server A ------------
947+ auto proxy_client_a = new_http_client ();
948+ DEFER (delete proxy_client_a);
949+ auto proxy_server_a_tcp = new_tcp_socket_server ();
950+ proxy_server_a_tcp->timeout (1000UL *1000 );
951+ proxy_server_a_tcp->bind_v4localhost ();
952+ proxy_server_a_tcp->listen ();
953+ DEFER (delete proxy_server_a_tcp);
954+ auto proxy_server_a = new_http_server ();
955+ DEFER (delete proxy_server_a);
956+ auto proxy_handler_a = new_proxy_handler (
957+ {source_server, &op_proxy_director},
958+ {(void *)" proxy-a" , &op_proxy_modifier}, proxy_client_a);
959+ proxy_server_a->add_handler (proxy_handler_a);
960+ proxy_server_a_tcp->set_handler (proxy_server_a->get_connection_handler ());
961+ proxy_server_a_tcp->start_loop ();
962+
963+ // --------start proxy server B ------------
964+ auto proxy_client_b = new_http_client ();
965+ DEFER (delete proxy_client_b);
966+ auto proxy_server_b_tcp = new_tcp_socket_server ();
967+ proxy_server_b_tcp->timeout (1000UL *1000 );
968+ proxy_server_b_tcp->bind_v4localhost ();
969+ proxy_server_b_tcp->listen ();
970+ DEFER (delete proxy_server_b_tcp);
971+ auto proxy_server_b = new_http_server ();
972+ DEFER (delete proxy_server_b);
973+ auto proxy_handler_b = new_proxy_handler (
974+ {source_server, &op_proxy_director},
975+ {(void *)" proxy-b" , &op_proxy_modifier}, proxy_client_b);
976+ proxy_server_b->add_handler (proxy_handler_b);
977+ proxy_server_b_tcp->set_handler (proxy_server_b->get_connection_handler ());
978+ proxy_server_b_tcp->start_loop ();
979+
980+ // --------client with no default proxy --------
981+ auto client = new_http_client ();
982+ DEFER (delete client);
983+
984+ // Operation 1: use proxy A
985+ auto op1 = client->new_operation (Verb::POST , to_url (source_server, " /echo" ));
986+ DEFER (client->destroy_operation (op1));
987+ op1->set_proxy (to_url (proxy_server_a_tcp, " /" ));
988+ std::string body1 = " 1234567890" ;
989+ op1->req .headers .content_length (body1.size ());
990+ auto writer1 = [&](Request* req) -> ssize_t {
991+ return req->write (body1.data (), body1.size ());
992+ };
993+ op1->body_writer = writer1;
994+ int ret1 = op1->call ();
995+ EXPECT_EQ (0 , ret1);
996+ // Verify request went through proxy A
997+ EXPECT_EQ (op1->resp .headers [" X-Via" ], " proxy-a" );
998+ // Verify echo body matches request body
999+ char buf1[4096 ] = {};
1000+ ret1 = op1->resp .read (buf1, sizeof (buf1));
1001+ EXPECT_EQ (ret1, (ssize_t )body1.size ());
1002+ EXPECT_EQ (std::string_view (buf1, ret1), body1);
1003+
1004+ // Operation 2: use proxy B
1005+ auto op2 = client->new_operation (Verb::POST , to_url (source_server, " /echo" ));
1006+ DEFER (client->destroy_operation (op2));
1007+ op2->set_proxy (to_url (proxy_server_b_tcp, " /" ));
1008+ std::string body2 = " abcdefghij" ;
1009+ op2->req .headers .content_length (body2.size ());
1010+ auto writer2 = [&](Request* req) -> ssize_t {
1011+ return req->write (body2.data (), body2.size ());
1012+ };
1013+ op2->body_writer = writer2;
1014+ int ret2 = op2->call ();
1015+ EXPECT_EQ (0 , ret2);
1016+ // Verify request went through proxy B
1017+ EXPECT_EQ (op2->resp .headers [" X-Via" ], " proxy-b" );
1018+ // Verify echo body matches request body
1019+ char buf2[4096 ] = {};
1020+ ret2 = op2->resp .read (buf2, sizeof (buf2));
1021+ EXPECT_EQ (ret2, (ssize_t )body2.size ());
1022+ EXPECT_EQ (std::string_view (buf2, ret2), body2);
1023+
1024+ // Operation 3: direct connection (no proxy)
1025+ auto op3 = client->new_operation (Verb::POST , to_url (source_server, " /echo" ));
1026+ DEFER (client->destroy_operation (op3));
1027+ std::string body3 = " ABCDEFGHIJ" ;
1028+ op3->req .headers .content_length (body3.size ());
1029+ auto writer3 = [&](Request* req) -> ssize_t {
1030+ return req->write (body3.data (), body3.size ());
1031+ };
1032+ op3->body_writer = writer3;
1033+ int ret3 = op3->call ();
1034+ EXPECT_EQ (0 , ret3);
1035+ // Verify request went directly (no X-Via header)
1036+ EXPECT_TRUE (op3->resp .headers [" X-Via" ].empty ());
1037+ EXPECT_EQ (200 , op3->resp .status_code ());
1038+ // Verify echo body matches request body
1039+ char buf3[4096 ] = {};
1040+ ret3 = op3->resp .read (buf3, sizeof (buf3));
1041+ EXPECT_EQ (ret3, (ssize_t )body3.size ());
1042+ EXPECT_EQ (std::string_view (buf3, ret3), body3);
1043+ }
1044+
8111045// Only for manual test
8121046// TEST(http_client, proxy) {
8131047// auto client = new_http_client();
0 commit comments