diff --git a/CHANGELOG.md b/CHANGELOG.md
index 025e6c79..b94c3573 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
  * Implement UI for selection of downloading specific mod versions (5d15eb0)
  * Change install & uninstall popups to reflect the new changes (d40f467)
  * Keep the scroll when enabling, installing etc. a mod (b3d444a)
+ * Update MacOS crashlog to include base and offset (7816c43)
 
 ## v1.2.1
  * Mods now target macOS 10.13 instead of 10.14 (7cc1cd4)
diff --git a/loader/include/Geode/utils/web.hpp b/loader/include/Geode/utils/web.hpp
index 4bf2d140..17896041 100644
--- a/loader/include/Geode/utils/web.hpp
+++ b/loader/include/Geode/utils/web.hpp
@@ -104,6 +104,16 @@ namespace geode::utils::web {
     template <class T>
     using DataConverter = Result<T> (*)(ByteVector const&);
 
+    // Hack until 2.0.0 to store extra members in AsyncWebRequest
+    struct AsyncWebRequestData {
+        std::variant<std::monostate, std::ostream*, ghc::filesystem::path> m_target;
+        std::string m_userAgent;
+        std::string m_customRequest;
+        bool m_isPostRequest = false;
+        std::string m_postFields;
+        bool m_isJsonRequest = false;
+    };
+
     /**
      * An asynchronous, thread-safe web request. Downloads data from the
      * internet without slowing the main thread. All callbacks are run in the
@@ -111,6 +121,9 @@ namespace geode::utils::web {
      */
     class GEODE_DLL AsyncWebRequest {
     private:
+        // i want to cry whose idea was to not make this pimpl
+        // For 2.0.0: make this pimpl
+        
         std::optional<std::string> m_joinID;
         std::string m_url;
         AsyncThen m_then = nullptr;
@@ -118,9 +131,12 @@ namespace geode::utils::web {
         AsyncProgress m_progress = nullptr;
         AsyncCancelled m_cancelled = nullptr;
         bool m_sent = false;
-        std::variant<std::monostate, std::ostream*, ghc::filesystem::path> m_target;
+        mutable std::variant<std::monostate, AsyncWebRequestData> m_extra;
         std::vector<std::string> m_httpHeaders;
 
+        AsyncWebRequestData& extra();
+        AsyncWebRequestData const& extra() const;
+
         template <class T>
         friend class AsyncWebResult;
         friend class SentAsyncWebRequest;
@@ -151,6 +167,29 @@ namespace geode::utils::web {
          * Can be called more than once.
          */
         AsyncWebRequest& header(std::string const& header);
+        /**
+         * In order to specify an user agent to the request, give it here.
+         */
+        AsyncWebRequest& userAgent(std::string const& userAgent);
+        /**
+         * Specify that the request is a POST request.
+         */
+        AsyncWebRequest& postRequest();
+        /**
+         * Specify that the request is a custom request like PUT and DELETE.
+         */
+        AsyncWebRequest& customRequest(std::string const& request);
+        /**
+         * Specify the post fields to send with the request. Only valid if
+         * `postRequest` or `customRequest` was called before.
+         */
+        AsyncWebRequest& postFields(std::string const& fields);
+        /**
+         * Specify the post fields to send with the request. Only valid if
+         * `postRequest` or `customRequest` was called before. Additionally
+         * sets the content type to application/json.
+         */
+        AsyncWebRequest& postFields(json::Value const& fields);
         /**
          * URL to fetch from the internet asynchronously
          * @param url URL of the data to download. Redirects will be
diff --git a/loader/src/loader/Index.cpp b/loader/src/loader/Index.cpp
index 97a2c410..d223ae0c 100644
--- a/loader/src/loader/Index.cpp
+++ b/loader/src/loader/Index.cpp
@@ -361,6 +361,7 @@ void Index::Impl::checkForUpdates() {
     auto oldSHA = file::readString(checksum).unwrapOr("");
     web::AsyncWebRequest()
         .join("index-update")
+        .userAgent("github_api/1.0")
         .header(fmt::format("If-None-Match: \"{}\"", oldSHA))
         .header("Accept: application/vnd.github.sha")
         .fetch("https://api.github.com/repos/geode-sdk/mods/commits/main")
diff --git a/loader/src/loader/LoaderImpl.cpp b/loader/src/loader/LoaderImpl.cpp
index ec523193..61afcb13 100644
--- a/loader/src/loader/LoaderImpl.cpp
+++ b/loader/src/loader/LoaderImpl.cpp
@@ -700,6 +700,7 @@ void Loader::Impl::fetchLatestGithubRelease(
     // TODO: add header to not get rate limited
     web::AsyncWebRequest()
         .join("loader-auto-update-check")
+        .userAgent("github_api/1.0")
         .fetch("https://api.github.com/repos/geode-sdk/geode/releases/latest")
         .json()
         .then([this, then](json::Value const& json) {
@@ -768,6 +769,7 @@ void Loader::Impl::downloadLoaderResources(bool useLatestRelease) {
     if (!useLatestRelease) {
         web::AsyncWebRequest()
             .join("loader-tag-exists-check")
+            .userAgent("github_api/1.0")
             .fetch(fmt::format(
                 "https://api.github.com/repos/geode-sdk/geode/git/ref/tags/{}",
                 this->getVersion().toString()
diff --git a/loader/src/utils/web.cpp b/loader/src/utils/web.cpp
index 72ca2eea..aab89b60 100644
--- a/loader/src/utils/web.cpp
+++ b/loader/src/utils/web.cpp
@@ -48,7 +48,6 @@ Result<> web::fetchFile(
     curl_easy_setopt(curl, CURLOPT_WRITEDATA, &file);
     curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeBinaryData);
     curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
-    curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0");
     if (prog) {
         curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
         curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, utils::fetch::progress);
@@ -81,7 +80,6 @@ Result<ByteVector> web::fetchBytes(std::string const& url) {
     curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
     curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret);
     curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeBytes);
-    curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0");
     auto res = curl_easy_perform(curl);
     if (res != CURLE_OK) {
         curl_easy_cleanup(curl);
@@ -120,7 +118,6 @@ Result<std::string> web::fetch(std::string const& url) {
     curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
     curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret);
     curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeString);
-    curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0");
     auto res = curl_easy_perform(curl);
     if (res != CURLE_OK) {
         curl_easy_cleanup(curl);
@@ -162,9 +159,9 @@ private:
     SentAsyncWebRequest* m_self;
 
     mutable std::mutex m_mutex;
-    std::variant<std::monostate, std::ostream*, ghc::filesystem::path> m_target =
-        std::monostate();
+    AsyncWebRequestData m_extra;
     std::vector<std::string> m_httpHeaders;
+    
 
     template <class T>
     friend class AsyncWebResult;
@@ -187,7 +184,7 @@ static std::unordered_map<std::string, SentAsyncWebRequestHandle> RUNNING_REQUES
 static std::mutex RUNNING_REQUESTS_MUTEX;
 
 SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const& req, std::string const& id) :
-    m_id(id), m_url(req.m_url), m_target(req.m_target), m_httpHeaders(req.m_httpHeaders) {
+    m_id(id), m_url(req.m_url), m_extra(req.extra()), m_httpHeaders(req.m_httpHeaders) {
 
 #define AWAIT_RESUME()    \
     {\
@@ -221,16 +218,16 @@ SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const
         std::unique_ptr<std::ofstream> file = nullptr;
 
         // into file
-        if (std::holds_alternative<ghc::filesystem::path>(m_target)) {
+        if (std::holds_alternative<ghc::filesystem::path>(m_extra.m_target)) {
             file = std::make_unique<std::ofstream>(
-                std::get<ghc::filesystem::path>(m_target), std::ios::out | std::ios::binary
+                std::get<ghc::filesystem::path>(m_extra.m_target), std::ios::out | std::ios::binary
             );
             curl_easy_setopt(curl, CURLOPT_WRITEDATA, file.get());
             curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeBinaryData);
         }
         // into stream
-        else if (std::holds_alternative<std::ostream*>(m_target)) {
-            curl_easy_setopt(curl, CURLOPT_WRITEDATA, std::get<std::ostream*>(m_target));
+        else if (std::holds_alternative<std::ostream*>(m_extra.m_target)) {
+            curl_easy_setopt(curl, CURLOPT_WRITEDATA, std::get<std::ostream*>(m_extra.m_target));
             curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, utils::fetch::writeBinaryData);
         }
         // into memory
@@ -242,8 +239,30 @@ SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const
         // No need to verify SSL, we trust our domains :-)
         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
         curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
-        // Github User Agent
-        curl_easy_setopt(curl, CURLOPT_USERAGENT, "github_api/1.0");
+        // User Agent
+        curl_easy_setopt(curl, CURLOPT_USERAGENT, m_extra.m_userAgent.c_str());
+
+        // Headers
+        curl_slist* headers = nullptr;
+        for (auto& header : m_httpHeaders) {
+            headers = curl_slist_append(headers, header.c_str());
+        }
+
+        // Post request
+        if (m_extra.m_isPostRequest || m_extra.m_customRequest.size()) {
+            if (m_extra.m_isPostRequest) {
+                curl_easy_setopt(curl, CURLOPT_POST, 1L);
+            }
+            else {
+                curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, m_extra.m_customRequest.c_str());
+            }
+            if (m_extra.m_isJsonRequest) {
+                headers = curl_slist_append(headers, "Content-Type: application/json");
+            }
+            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, m_extra.m_postFields.c_str());
+            curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, m_extra.m_postFields.size());
+        }
+
         // Track progress
         curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
         // Follow redirects
@@ -251,10 +270,7 @@ SentAsyncWebRequest::Impl::Impl(SentAsyncWebRequest* self, AsyncWebRequest const
         // Fail if response code is 4XX or 5XX
         curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
 
-        curl_slist* headers = nullptr;
-        for (auto& header : m_httpHeaders) {
-            headers = curl_slist_append(headers, header.c_str());
-        }
+        // Headers end
         curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
 
         struct ProgressData {
@@ -318,8 +334,8 @@ void SentAsyncWebRequest::Impl::doCancel() {
     m_cleanedUp = true;
 
     // remove file if downloaded to one
-    if (std::holds_alternative<ghc::filesystem::path>(m_target)) {
-        auto path = std::get<ghc::filesystem::path>(m_target);
+    if (std::holds_alternative<ghc::filesystem::path>(m_extra.m_target)) {
+        auto path = std::get<ghc::filesystem::path>(m_extra.m_target);
         if (ghc::filesystem::exists(path)) {
             try {
                 ghc::filesystem::remove(path);
@@ -410,11 +426,50 @@ void SentAsyncWebRequest::error(std::string const& error, int code) {
     return m_impl->error(error, code);
 }
 
+AsyncWebRequestData& AsyncWebRequest::extra() {
+    if (!std::holds_alternative<AsyncWebRequestData>(m_extra)) {
+        m_extra = AsyncWebRequestData();
+    }
+    return std::get<AsyncWebRequestData>(m_extra);
+}
+
+AsyncWebRequestData const& AsyncWebRequest::extra() const {
+    if (!std::holds_alternative<AsyncWebRequestData>(m_extra)) {
+        m_extra = AsyncWebRequestData();
+    }
+    return std::get<AsyncWebRequestData>(m_extra);
+}
+
 AsyncWebRequest& AsyncWebRequest::join(std::string const& requestID) {
     m_joinID = requestID;
     return *this;
 }
 
+AsyncWebRequest& AsyncWebRequest::userAgent(std::string const& userAgent) {
+    this->extra().m_userAgent = userAgent;
+    return *this;
+}
+
+AsyncWebRequest& AsyncWebRequest::postRequest() {
+    this->extra().m_isPostRequest = true;
+    return *this;
+}
+
+AsyncWebRequest& AsyncWebRequest::customRequest(std::string const& request) {
+    this->extra().m_customRequest = request;
+    return *this;
+}
+
+AsyncWebRequest& AsyncWebRequest::postFields(std::string const& fields) {
+    this->extra().m_postFields = fields;
+    return *this;
+}
+
+AsyncWebRequest& AsyncWebRequest::postFields(json::Value const& fields) {
+    this->extra().m_isJsonRequest = true;
+    return this->postFields(fields.dump());
+}
+
 AsyncWebRequest& AsyncWebRequest::header(std::string const& header) {
     m_httpHeaders.push_back(header);
     return *this;
@@ -489,14 +544,14 @@ AsyncWebRequest::~AsyncWebRequest() {
 }
 
 AsyncWebResult<std::monostate> AsyncWebResponse::into(std::ostream& stream) {
-    m_request.m_target = &stream;
+    m_request.extra().m_target = &stream;
     return this->as(+[](ByteVector const&) -> Result<std::monostate> {
         return Ok(std::monostate());
     });
 }
 
 AsyncWebResult<std::monostate> AsyncWebResponse::into(ghc::filesystem::path const& path) {
-    m_request.m_target = path;
+    m_request.extra().m_target = path;
     return this->as(+[](ByteVector const&) -> Result<std::monostate> {
         return Ok(std::monostate());
     });