// file : mod/mod-ci-github-post.hxx -*- C++ -*- // license : MIT; see accompanying LICENSE file #ifndef MOD_MOD_CI_GITHUB_POST_HXX #define MOD_MOD_CI_GITHUB_POST_HXX #include #include #include namespace brep { // Send a POST request to the GitHub API endpoint `ep`, parse GitHub's JSON // response into `rs` (only for 200 codes), and return the HTTP status code. // // The endpoint `ep` should not have a leading slash. // // Pass additional HTTP headers in `hdrs`. For example: // // "HeaderName: header value" // // Throw invalid_argument if unable to parse the response headers, // invalid_json_input (derived from invalid_argument) if unable to parse the // response body, and system_error in other cases. // template uint16_t github_post (T& rs, const string& ep, const strings& hdrs, const string& body = "") { using namespace butl; // Convert the header values to curl header option/value pairs. // strings hdr_opts; for (const string& h: hdrs) { hdr_opts.push_back ("--header"); hdr_opts.push_back (h); } // Run curl. // try { // Pass --include to print the HTTP status line (followed by the response // headers) so that we can get the response status code. // // Suppress the --fail option which causes curl to exit with status 22 // in case of an error HTTP response status code (>= 400) otherwise we // can't get the status code. // // Note that butl::curl also adds --location to make curl follow redirects // (which is recommended by GitHub). // // The API version `2022-11-28` is the only one currently supported. If // the X-GitHub-Api-Version header is not passed this version will be // chosen by default. // fdpipe errp (fdopen_pipe ()); // stderr pipe. curl c (path ("-"), // Read input from curl::out. path ("-"), // Write response to curl::in. process::pipe (errp.in.get (), move (errp.out)), curl::post, curl::flags::no_fail, "https://api.github.com/" + ep, "--no-fail", // Don't fail if response status code >= 400. "--include", // Output response headers for status code. "--header", "Accept: application/vnd.github+json", "--header", "X-GitHub-Api-Version: 2022-11-28", move (hdr_opts)); ifdstream err (move (errp.in)); // Parse the HTTP response. // uint16_t sc; // Status code. try { // Note: re-open in/out so that they get automatically closed on // exception. // ifdstream in (c.in.release (), fdstream_mode::skip); ofdstream out (c.out.release ()); // Write request body to out. // if (!body.empty ()) out << body; out.close (); sc = curl::read_http_status (in).code; // May throw invalid_argument. // Parse the response body if the status code is in the 200 range. // if (sc >= 200 && sc < 300) { // Use endpoint name as input name (useful to have it propagated // in exceptions). // json::parser p (in, ep /* name */); rs = T (p); } in.close (); } catch (const io_error& e) { // If the process exits with non-zero status, assume the IO error is due // to that and fall through. // if (c.wait ()) { throw_generic_error ( e.code ().value (), (string ("unable to read curl stdout: ") + e.what ()).c_str ()); } } catch (const json::invalid_json_input&) { // If the process exits with non-zero status, assume the JSON error is // due to that and fall through. // if (c.wait ()) throw; } if (!c.wait ()) { string et (err.read_text ()); throw_generic_error (EINVAL, ("non-zero curl exit status: " + et).c_str ()); } err.close (); return sc; } catch (const process_error& e) { throw_generic_error ( e.code ().value (), (string ("unable to execute curl:") + e.what ()).c_str ()); } catch (const io_error& e) { // Unable to read diagnostics from stderr. // throw_generic_error ( e.code ().value (), (string ("unable to read curl stderr : ") + e.what ()).c_str ()); } } } #endif // MOD_MOD_CI_GITHUB_POST_HXX