Repository: xrootd/xrootd
Pull request #2496: Add webdav error codes for file path and permission errors
Author: login: dynamic-entropy
Created: 2025-04-25T12:31:31Z | Merged: 2025-05-30T08:08:19Z

Related Issue: #2261 

This PR introduces a new `XrdHttpError` class to standardise HTTP error handling and integrates it into the HTTP request processing workflow.

* Add the `XrdHttpError` class in `src/XrdHttp/XrdHttpError.hh` to encapsulate HTTP error responses. 
* Build on top of the `XrdOssTests` module to simulate filesystem errors (e.g., no space, quota exceeded) for testing purposes.
* Expanded the HTTP test suite (`tests/XRootD/http.sh`) with new test cases to handle errors for destination file/path category as per the webdav error improvement document.
https://twiki.cern.ch/twiki/bin/view/LCG/WebdavErrorImprovement 



Diff:
diff --git a/src/XrdHttp/XrdHttpProtocol.cc b/src/XrdHttp/XrdHttpProtocol.cc
index 8fd6b933daa..d9ac455946b 100644
--- a/src/XrdHttp/XrdHttpProtocol.cc
+++ b/src/XrdHttp/XrdHttpProtocol.cc
@@ -1589,35 +1589,47 @@ int XrdHttpProtocol::SendData(const char *body, int bodylen) {
 /******************************************************************************/
 /*                       S t a r t S i m p l e R e s p                        */
 /******************************************************************************/
-  
-int XrdHttpProtocol::StartSimpleResp(int code, const char *desc, const char *header_to_add, long long bodylen, bool keepalive) {
+
+int XrdHttpProtocol::StartSimpleResp(int code, const char *desc,
+                                     const char *header_to_add,
+                                     long long bodylen, bool keepalive) {
+  static const std::unordered_map<int, std::string> statusTexts = {
+      {100, "Continue"},
+      {200, "OK"},
+      {201, "Created"},
+      {206, "Partial Content"},
+      {302, "Redirect"},
+      {307, "Temporary Redirect"},
+      {400, "Bad Request"},
+      {401, "Unauthorized"},
+      {403, "Forbidden"},
+      {404, "Not Found"},
+      {405, "Method Not Allowed"},
+      {409, "Conflict"},
+      {416, "Range Not Satisfiable"},
+      {423, "Locked"},
+      {500, "Internal Server Error"},
+      {502, "Bad Gateway"},
+      {504, "Gateway Timeout"},
+      {507, "Insufficient Storage"}};
+
   std::stringstream ss;
   const std::string crlf = "\r\n";
 
   ss << "HTTP/1.1 " << code << " ";
+
   if (desc) {
     ss << desc;
   } else {
-    if (code == 200) ss << "OK";
-    else if (code == 100) ss << "Continue";
-    else if (code == 201) ss << "Created";
-    else if (code == 206) ss << "Partial Content";
-    else if (code == 302) ss << "Redirect";
-    else if (code == 307) ss << "Temporary Redirect";
-    else if (code == 400) ss << "Bad Request";
-    else if (code == 401) ss << "Unauthorized";
-    else if (code == 403) ss << "Forbidden";
-    else if (code == 404) ss << "Not Found";
-    else if (code == 405) ss << "Method Not Allowed";
-    else if (code == 409) ss << "Conflict";
-    else if (code == 416) ss << "Range Not Satisfiable";
-    else if (code == 423) ss << "Locked";
-    else if (code == 500) ss << "Internal Server Error";
-    else if (code == 502) ss << "Bad Gateway";
-    else if (code == 504) ss << "Gateway Timeout";
-    else ss << "Unknown";
+    auto it = statusTexts.find(code);
+    if (it != statusTexts.end()) {
+      ss << it->second;
+    } else {
+      ss << "Unknown";
+    }
   }
   ss << crlf;
+
   if (keepalive && (code != 100))
     ss << "Connection: Keep-Alive" << crlf;
   else
@@ -1631,19 +1643,18 @@ int XrdHttpProtocol::StartSimpleResp(int code, const char *desc, const char *hea
   } else {
     ss << m_staticheaders[""];
   }
-  
+
   if ((bodylen >= 0) && (code != 100))
     ss << "Content-Length: " << bodylen << crlf;
 
-  if (header_to_add && (header_to_add[0] != '\0'))
-    ss << header_to_add << crlf;
+  if (header_to_add && (header_to_add[0] != '\0')) ss << header_to_add << crlf;
 
   ss << crlf;
 
   const std::string &outhdr = ss.str();
   TRACEI(RSP, "Sending resp: " << code << " header len:" << outhdr.size());
   if (SendData(outhdr.c_str(), outhdr.size()))
-    return -1;
+   return -1;
 
   return 0;
 }
diff --git a/src/XrdHttp/XrdHttpReq.cc b/src/XrdHttp/XrdHttpReq.cc
index 18ea437e5fc..dcaed7f98f6 100644
--- a/src/XrdHttp/XrdHttpReq.cc
+++ b/src/XrdHttp/XrdHttpReq.cc
@@ -818,38 +818,89 @@ void XrdHttpReq::parseResource(char *res) {
   
 }
 
+void XrdHttpReq::sendWebdavErrorMessage(
+    XResponseType xrdresp, XErrorCode xrderrcode, XrdHttpReq::ReqType httpVerb,
+    XRequestTypes xrdOperation, std::string etext, const char *desc,
+    const char *header_to_add, bool keepalive) {
+  int code{0};
+  std::string errCode{"Unknown"};
+  std::string statusText;
+
+  switch (httpVerb) {
+    case XrdHttpReq::rtPUT:
+      if (xrdOperation == kXR_open) {
+        if (xrderrcode == kXR_isDirectory) {
+          code = 409;
+          errCode = "8.1";
+        } else if (xrderrcode == kXR_NoSpace) {
+          code = 507;
+          errCode = "8.3.1";
+        } else if (xrderrcode == kXR_overQuota) {
+          code = 507;
+          errCode = "8.3.2";
+        } else if (xrderrcode == kXR_NotAuthorized) {
+          code = 403;
+          errCode = "9.3";
+        }
+      } else if (xrdOperation == kXR_write) {
+        if (xrderrcode == kXR_NoSpace) {
+          code = 507;
+          errCode = "8.4.1";
+        } else if (xrderrcode == kXR_overQuota) {
+          code = 507;
+          errCode = "8.4.2";
+        }
+      }
+      break;
+    default:
+      break;
+  }
+
+  // Remove the if at the end of project completion
+  // Till then status text defaults to as set by mapXrdResponseToHttpStatus
+  if (code != 0) {
+    httpStatusCode = code;
+    httpErrorCode = errCode;
+    httpErrorBody = "ERROR: " + errCode + ": " + etext + "\n";
+
+    prot->SendSimpleResp(httpStatusCode, desc, header_to_add,
+                         httpErrorBody.c_str(), httpErrorBody.length(),
+                         keepalive);
+  }
+}
+
 // Map an XRootD error code to an appropriate HTTP status code and message
-// The variables httpStatusCode and httpStatusText will be populated
+// The variables httpStatusCode and httpErrorBody will be populated
 
 void XrdHttpReq::mapXrdErrorToHttpStatus() {
   // Set default HTTP status values for an error case
   httpStatusCode = 500;
-  httpStatusText = "Unrecognized error";
+  httpErrorBody = "Unrecognized error";
 
   // Do error mapping
   if (xrdresp == kXR_error) {
     switch (xrderrcode) {
       case kXR_AuthFailed:
-        httpStatusCode = 401; httpStatusText = "Unauthorized";
+        httpStatusCode = 401; httpErrorBody = "Unauthorized";
         break;
       case kXR_NotAuthorized:
-        httpStatusCode = 403; httpStatusText = "Operation not permitted";
+        httpStatusCode = 403; httpErrorBody = "Operation not permitted";
         break;
       case kXR_NotFound:
-        httpStatusCode = 404; httpStatusText = "File not found";
+        httpStatusCode = 404; httpErrorBody = "File not found";
         break;
       case kXR_Unsupported:
-        httpStatusCode = 405; httpStatusText = "Operation not supported";
+        httpStatusCode = 405; httpErrorBody = "Operation not supported";
         break;
       case kXR_FileLocked:
-        httpStatusCode = 423; httpStatusText = "Resource is a locked";
+        httpStatusCode = 423; httpErrorBody = "Resource is a locked";
         break;
       case kXR_isDirectory:
-        httpStatusCode = 409; httpStatusText = "Resource is a directory";
+        httpStatusCode = 409; httpErrorBody = "Resource is a directory";
         break;
       case kXR_ItExists:
         if(request != ReqType::rtDELETE) {
-          httpStatusCode = 409; httpStatusText = "File already exists";
+          httpStatusCode = 409; httpErrorBody = "File already exists";
         } else {
           // In the case the XRootD layer returns a kXR_ItExists after a deletion
           // was submitted, we return a 405 status code with the error message set by
@@ -858,27 +909,27 @@ void XrdHttpReq::mapXrdErrorToHttpStatus() {
         }
         break;
       case kXR_InvalidRequest:
-        httpStatusCode = 405; httpStatusText = "Method is not allowed";
+        httpStatusCode = 405; httpErrorBody = "Method is not allowed";
         break;
       case kXR_noserver:
-        httpStatusCode = 502; httpStatusText = "Bad Gateway";
+        httpStatusCode = 502; httpErrorBody = "Bad Gateway";
         break;
       case kXR_TimerExpired:
-        httpStatusCode = 504; httpStatusText = "Gateway timeout";
+        httpStatusCode = 504; httpErrorBody = "Gateway timeout";
         break;
       default:
         break;
     }
 
-    if (!etext.empty()) httpStatusText = etext;
+    if (!etext.empty()) httpErrorBody = etext;
 
     TRACEI(REQ, "PostProcessHTTPReq mapping Xrd error [" << xrderrcode
                  << "] to status code [" << httpStatusCode << "]");
 
-    httpStatusText += "\n";
+    httpErrorBody += "\n";
   } else {
       httpStatusCode = 200;
-      httpStatusText = "OK";
+      httpErrorBody = "OK";
   }
 }
 
@@ -1176,8 +1227,9 @@ int XrdHttpReq::ProcessHTTPReq() {
 
             if (!prot->Bridge->Run((char *) &xrdreq, (char *) res.c_str(), l)) {
               mapXrdErrorToHttpStatus();
-              prot->SendSimpleResp(httpStatusCode, NULL, NULL, httpStatusText.c_str(), httpStatusText.length(), false);
-              return sendFooterError("Could not run listing request on the bridge");
+              prot->SendSimpleResp(httpStatusCode, NULL, NULL, httpErrorBody.c_str(), httpErrorBody.length(), false);
+              sendFooterError("Could not run listing request on the bridge");
+              return -1;
             }
 
             // We don't want to be invoked again after this request is finished
@@ -1269,7 +1321,7 @@ int XrdHttpReq::ProcessHTTPReq() {
 
             if ((offs >= filesize) || (offs+l > filesize)) {
               httpStatusCode = 416;
-              httpStatusText = "Range Not Satisfiable";
+              httpErrorBody = "Range Not Satisfiable";
               std::stringstream ss;
               ss << "Requested range " << l << "@" << offs << " is past the end of file (" << filesize << ")";
               return sendFooterError(ss.str());
@@ -1767,7 +1819,7 @@ XrdHttpReq::PostProcessChecksum(std::string &digest_header) {
     if (convert_to_base64) {free(digest_value);}
     return 0;
   } else {
-    prot->SendSimpleResp(httpStatusCode, NULL, NULL, httpStatusText.c_str(), httpStatusText.length(), false);
+    prot->SendSimpleResp(httpStatusCode, NULL, NULL, httpErrorBody.c_str(), httpErrorBody.length(), false);
     return -1;
   }
 }
@@ -1777,7 +1829,7 @@ XrdHttpReq::PostProcessListing(bool final_) {
 
   if (xrdresp == kXR_error) {
     prot->SendSimpleResp(httpStatusCode, NULL, NULL,
-                          httpStatusText.c_str(), httpStatusText.length(), false);
+                          httpErrorBody.c_str(), httpErrorBody.length(), false);
     return -1;
   }
 
@@ -2209,7 +2261,7 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
           } else { // xrdresp indicates an error occurred
 
             prot->SendSimpleResp(httpStatusCode, NULL, NULL,
-                                  httpStatusText.c_str(), httpStatusText.length(), false);
+                                  httpErrorBody.c_str(), httpErrorBody.length(), false);
             return -1;
           }
           // Case should not be reachable
@@ -2223,7 +2275,7 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
         {
           if (xrdresp != kXR_ok) {
             prot->SendSimpleResp(httpStatusCode, NULL, NULL,
-                                  httpStatusText.c_str(), httpStatusText.length(), false);
+                                  httpErrorBody.c_str(), httpErrorBody.length(), false);
             return -1;
           }
           return 0;
@@ -2247,7 +2299,7 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
             const XrdHttpReadRangeHandler::Error &rrerror = readRangeHandler.getError();
             if (rrerror) {
               httpStatusCode = rrerror.httpRetCode;
-              httpStatusText = rrerror.errMsg;
+              httpErrorBody = rrerror.errMsg;
             }
               
             if (m_transfer_encoding_chunked && m_trailer_headers) {
@@ -2256,7 +2308,7 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
 
               const std::string crlf = "\r\n";
               std::stringstream ss;
-              ss << "X-Transfer-Status: " << httpStatusCode << ": " << httpStatusText << crlf;
+              ss << "X-Transfer-Status: " << httpStatusCode << ": " << httpErrorBody << crlf;
 
               const auto header = ss.str();
               if (prot->SendData(header.c_str(), header.size()))
@@ -2309,11 +2361,9 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
     case XrdHttpReq::rtPUT:
     {
       if (!fopened) {
-
         if (xrdresp != kXR_ok) {
-
-          prot->SendSimpleResp(httpStatusCode, NULL, NULL,
-                               httpStatusText.c_str(), httpStatusText.length(), keepalive);
+          sendWebdavErrorMessage(xrdresp, xrderrcode, XrdHttpReq::rtPUT,
+                                 kXR_open, etext, NULL, NULL, keepalive);
           return -1;
         }
 
@@ -2355,16 +2405,14 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
 
         if (ntohs(xrdreq.header.requestid) == kXR_close) {
           if (xrdresp == kXR_ok) {
-            prot->SendSimpleResp(201, NULL, NULL, (char *) ":-)", 0, keepalive);
+            prot->SendSimpleResp(201, NULL, NULL, (char *)":-)", 0, keepalive);
             return keepalive ? 1 : -1;
           } else {
-            prot->SendSimpleResp(httpStatusCode, NULL, NULL,
-                                 httpStatusText.c_str(), httpStatusText.length(), keepalive);
+            sendWebdavErrorMessage(xrdresp, xrderrcode, XrdHttpReq::rtPUT,
+                                   kXR_close, etext, NULL, NULL, keepalive);
             return -1;
           }
         }
-
-
       }
 
 
@@ -2381,7 +2429,7 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
 
       if (xrdresp != kXR_ok) {
         prot->SendSimpleResp(httpStatusCode, NULL, NULL,
-                             httpStatusText.c_str(), httpStatusText.length(), keepalive);
+                             httpErrorBody.c_str(), httpErrorBody.length(), keepalive);
         return -1;
       }
 
@@ -2415,7 +2463,7 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
             return keepalive ? 1 : -1;
           }
           prot->SendSimpleResp(httpStatusCode, NULL, NULL,
-                               httpStatusText.c_str(), httpStatusText.length(), keepalive);
+                               httpErrorBody.c_str(), httpErrorBody.length(), keepalive);
           return -1;
         }
       }
@@ -2428,7 +2476,7 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
 
       if (xrdresp == kXR_error) {
         prot->SendSimpleResp(httpStatusCode, NULL, NULL,
-                             httpStatusText.c_str(), httpStatusText.length(), false);
+                             httpErrorBody.c_str(), httpErrorBody.length(), false);
         return -1;
       }
 
@@ -2663,7 +2711,7 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
           prot->SendSimpleResp(405, NULL, NULL, (char *) "Method is not allowed; resource already exists.", 0, false);
         } else {
           prot->SendSimpleResp(httpStatusCode, NULL, NULL,
-                               httpStatusText.c_str(), httpStatusText.length(), false);
+                               httpErrorBody.c_str(), httpErrorBody.length(), false);
         }
         return -1;
       }
@@ -2694,7 +2742,7 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
   switch (xrdresp) {
     case kXR_error:
       prot->SendSimpleResp(httpStatusCode, NULL, NULL,
-                           httpStatusText.c_str(), httpStatusText.length(), false);
+                           httpErrorBody.c_str(), httpErrorBody.length(), false);
       return -1;
       break;
 
@@ -2721,17 +2769,19 @@ XrdHttpReq::sendFooterError(const std::string &extra_text) {
       return -1;
 
     std::stringstream ss;
+
     ss << httpStatusCode;
-    if (!httpStatusText.empty()) {
-      std::string_view statusView(httpStatusText);
+    if (!httpErrorBody.empty()) {
+      std::string_view statusView(httpErrorBody);
       // Remove trailing newline; this is not valid in a trailer value
       // and causes incorrect framing of the response, confusing clients.
       if (statusView[statusView.size() - 1] == '\n') {
         ss << ": " << statusView.substr(0, statusView.size() - 1);
       } else {
-        ss << ": " << httpStatusText;
+        ss << ": " << httpErrorBody;
       }
     }
+
     if (!extra_text.empty())
       ss << ": " << extra_text;
     TRACEI(REQ, ss.str());
@@ -2746,8 +2796,9 @@ XrdHttpReq::sendFooterError(const std::string &extra_text) {
 
     return keepalive ? 1 : -1;
   } else {
-    TRACEI(REQ, "Failure during response: " << httpStatusCode << ": " << httpStatusText << (extra_text.empty() ? "" : (": " + extra_text)));
+    TRACEI(REQ, "Failure during response: " << httpStatusCode << ": " << httpErrorBody << (extra_text.empty() ? "" : (": " + extra_text)));
     return -1;
+
   }
 }
 
diff --git a/src/XrdHttp/XrdHttpReq.hh b/src/XrdHttp/XrdHttpReq.hh
index 72dd805fe82..7a8fb32a337 100644
--- a/src/XrdHttp/XrdHttpReq.hh
+++ b/src/XrdHttp/XrdHttpReq.hh
@@ -69,10 +69,48 @@ class XrdHttpProtocol;
 class XrdOucEnv;
 
 class XrdHttpReq : public XrdXrootd::Bridge::Result {
+
+public:
+  // ----------------
+  // Description of the request. The header/body parsing
+  // is supposed to populate these fields, for fast access while
+  // processing the request
+
+  /// These are the HTTP/DAV requests that we support
+
+  enum ReqType: int {
+    rtUnset = -1,
+    rtUnknown = 0,
+    rtMalformed,
+    rtGET,
+    rtHEAD,
+    rtPUT,
+    rtOPTIONS,
+    rtPATCH,
+    rtDELETE,
+    rtPROPFIND,
+    rtMKCOL,
+    rtMOVE,
+    rtPOST
+  };
+
 private:
   // HTTP response parameters to be sent back to the user
   int httpStatusCode;
-  std::string httpStatusText;
+  // HTTP Error code for the response
+  // e.g. 8.1, 8.3.1, etc.
+  // https://twiki.cern.ch/twiki/bin/view/LCG/WebdavErrorImprovement
+  std::string httpErrorCode;
+  // HTTP response text with following format:
+  // Severity: ErrorCode: free-style text message
+  // Severity being OK, WARNING, or ERROR
+  // ErrorCode being a decimal numeric plus dot string, i.e. n or n.m or n.m.l,
+  // etc. free-style text message any UTF-8 string
+  // Optionally, it also contains the trailer headers whereever applicable
+  // e.g. X-Transfer-Status: 200: OK
+  // or X-Transfer-Status: 500: ERROR: <error message>: <additional text>
+  std::string httpErrorBody;
+
 
   // The value of the user agent, if specified
   std::string m_user_agent;
@@ -126,7 +164,13 @@ private:
   void parseResource(char *url);
   // Map an XRootD error code to an appropriate HTTP status code and message
   void mapXrdErrorToHttpStatus();
-  
+
+  // Set Webdav Error messages
+  void sendWebdavErrorMessage(XResponseType xrdresp, XErrorCode xrderrcode,
+                              ReqType httpVerb, XRequestTypes xrdOperation,
+                              std::string etext, const char *desc,
+                              const char *header_to_add, bool keepalive);
+
   // Sanitize the resource from http[s]://[host]/ questionable prefix
   void sanitizeResourcePfx();
 
@@ -214,28 +258,6 @@ public:
   // Return the current user agent; if none has been specified, returns an empty string
   const std::string &userAgent() const {return m_user_agent;}
 
-  // ----------------
-  // Description of the request. The header/body parsing
-  // is supposed to populate these fields, for fast access while
-  // processing the request
-
-  /// These are the HTTP/DAV requests that we support
-
-  enum ReqType {
-    rtUnset = -1,
-    rtUnknown = 0,
-    rtMalformed,
-    rtGET,
-    rtHEAD,
-    rtPUT,
-    rtOPTIONS,
-    rtPATCH,
-    rtDELETE,
-    rtPROPFIND,
-    rtMKCOL,
-    rtMOVE,
-    rtPOST
-  };
 
   /// The request we got
   ReqType request;
diff --git a/tests/XRootD/http.cfg b/tests/XRootD/http.cfg
index 128e1253bea..a744d8dcaea 100644
--- a/tests/XRootD/http.cfg
+++ b/tests/XRootD/http.cfg
@@ -13,6 +13,7 @@ http.desthttps false
 http.selfhttps2http false
 
 ofs.authlib libXrdMacaroons.so
+ofs.osslib ++ libXrdOssTests.so
 http.header2cgi Authorization authz
 http.exthandler xrdtpc libXrdHttpTPC.so
 http.exthandler xrdmacaroons libXrdMacaroons.so
diff --git a/tests/XRootD/http.sh b/tests/XRootD/http.sh
index 289d8e0efab..d2604133570 100755
--- a/tests/XRootD/http.sh
+++ b/tests/XRootD/http.sh
@@ -206,4 +206,61 @@ function test_http() {
   assert_eq "2" "$(grep -B 1 "X-Transfer-Status: 500: Unable to read" "$outputFilePath" | grep -c -E "^0")" "$(sed -e 's/blah//g' < "$outputFilePath")"
   assert_eq "0" "$(grep -c "Leftovers after chunking" "${TMPDIR}/stderr.txt")" "Incorrect framing in response: $(sed -e 's/blah//g' < "${TMPDIR}/stderr.txt")"
   assert_eq "0" "$(grep -c "Connection died" "${TMPDIR}/stderr.txt")" "Connection reuse did not work.  Server log: $(cat "${XROOTD_SERVER_LOGFILE}") Client log: $(sed -e 's/blah//g' < "${TMPDIR}/stderr.txt") Issue:"
+  
+
+  run_and_assert_http_and_error_code() {
+    local expected_http_code="$1"
+    local expected_error_code="$2"
+    shift 2
+
+    local body_file
+    body_file=$(mktemp)
+    local http_code
+
+    # Run the curl command, capture HTTP code and body
+    http_code=$(curl -s -v -L -w "%{http_code}" -o "$body_file" "$@")
+    local body
+    body=$(< "$body_file")
+    rm -f "$body_file"
+
+    # Assertions
+    assert_eq "$expected_http_code" "$http_code"
+
+    # Only assert on error code if HTTP status is 400+
+    if [[ "$http_code" -ge 400 && -n "$expected_error_code" ]]; then
+      local error_code
+      error_code=$(echo "$body" | grep -oE 'ERROR: [0-9]+(\.[0-9]+){1,2}' | awk '{print $2}')
+      # Print body only if assertion fails
+      assert_eq "$expected_error_code" "$error_code" "$body" 
+    fi
+  }
+
+  # Overwrite a directory with a file - File / Directory conflict
+  run_and_assert_http_and_error_code 409 "8.1" \
+    --upload-file "$alphabetFilePath" "${HOST}/$TMPDIR"
+
+  # Upload a file that should fail due to insufficient space
+  # The server can only close the connection if no space if left mid write
+  # noSpaceFilePath="$TMPDIR/no_space.txt"
+  # run_and_assert_http_and_error_code 507 "8.4.1" \
+  #   --upload-file "$alphabetFilePath" "${HOST}/$noSpaceFilePath"
+
+  # Upload a file that should fail due to insufficient inodes
+  noInodeFilePath="$TMPDIR/no_inode.txt"
+  run_and_assert_http_and_error_code 507 "8.3.1" \
+    --upload-file "$alphabetFilePath" "${HOST}/$noInodeFilePath"
+
+  # # Fail upload due to insufficient user quota for space
+  # Not handled yet - connection is closed instead
+  # outOfSpaceQuotaFilePath="$TMPDIR/out_of_space_quota.txt"
+  # run_and_assert_http_and_error_code 507 "8.4.2" \
+  #   --upload-file "$alphabetFilePath" "${HOST}/$outOfSpaceQuotaFilePath"
+
+  # Fail upload due to insufficient user quota for inodes
+  outOfInodeQuotaFilePath="$TMPDIR/out_of_inode_quota.txt"
+  run_and_assert_http_and_error_code 507 "8.3.2" \
+    --upload-file "$alphabetFilePath" "${HOST}/$outOfInodeQuotaFilePath"
+
+  run_and_assert_http_and_error_code 200 "" \
+    --header "Want-Digest: crc32c" -I "${HOST}/$alphabetFilePath"
 }
diff --git a/tests/XrdOssTests/CMakeLists.txt b/tests/XrdOssTests/CMakeLists.txt
index b1f82da091f..71ec1e8606a 100644
--- a/tests/XrdOssTests/CMakeLists.txt
+++ b/tests/XrdOssTests/CMakeLists.txt
@@ -5,8 +5,7 @@
 # the testing of failures
 #
 
-add_library( XrdOssTests MODULE xrdoss_tests.cc )
+add_library( XrdOssTests MODULE XrdOssTests.cc )
 target_link_libraries( XrdOssTests XrdServer )
 
 set_target_properties( XrdOssTests PROPERTIES OUTPUT_NAME "XrdOssTests-${PLUGIN_VERSION}" SUFFIX ".so" )
-
diff --git a/tests/XrdOssTests/xrdoss_tests.cc b/tests/XrdOssTests/XrdOssTests.cc
similarity index 68%
rename from tests/XrdOssTests/xrdoss_tests.cc
rename to tests/XrdOssTests/XrdOssTests.cc
index 5bd3c83f1af..aef116bc427 100644
--- a/tests/XrdOssTests/xrdoss_tests.cc
+++ b/tests/XrdOssTests/XrdOssTests.cc
@@ -23,19 +23,31 @@ class File final : public XrdOssWrapDF {
         std::string path_str(path);
         auto const pos = path_str.find_last_of('/');
         const auto leaf = path_str.substr(pos + 1);
+
         m_fail = leaf == "fail_read.txt";
+
+        if (leaf == "no_space.txt") errorCode = ENOSPC;
+        else if (leaf == "out_of_space_quota.txt") errorCode = EDQUOT;
         return wrapDF.Open(path, Oflag, Mode, env);
     }
 
     ssize_t Read(void *buffer, off_t offset, size_t size) override {
         if (m_fail && offset > 0) return -EIO;
+
         return wrapDF.Read(buffer, offset, size);
     }
 
+    ssize_t Write(const void *buffer, off_t offset, size_t size) override {
+        if (errorCode >= 0) return -errorCode;
+
+        return wrapDF.Write(buffer, offset, size);
+    }
+
     int getFD() override {return -1;}
 
   private:
     bool m_fail{false};
+    int errorCode{-1};
     std::unique_ptr<XrdOssDF> m_wrapped;
 };
 
@@ -51,6 +63,19 @@ class FileSystem final : public XrdOssWrapper {
         return new File(std::move(wrapped));
     }
 
+    int Create(const char *tid, const char *path, mode_t mode,
+      XrdOucEnv &env, int opts=0) override {
+        std::string path_str(path);
+        auto const pos = path_str.find_last_of('/');
+        const auto filename = path_str.substr(pos + 1);
+
+        if (filename == "no_inode.txt" ) return -ENOSPC;
+        else if (filename == "out_of_inode_quota.txt") return -EDQUOT;
+        return wrapPI.Create(tid, path, mode, env, opts);
+      }
+
+
+
   private:
     std::unique_ptr<XrdOss> m_oss;
 };
@@ -68,4 +93,3 @@ XrdOss *XrdOssAddStorageSystem2(XrdOss *curr_oss, XrdSysLogger *logger,
 XrdVERSIONINFO(XrdOssAddStorageSystem2, slowfs);
 
 } // extern "C"
-


Reviews:
user: login: amadio
body: This is a good start, but I think that a class is not necessary, we can have a table with all the errors in it, a function to index into this table to get the actual error properties, and another function to format the string and call `prot->SendSimpleResp(...)`. We should also check the return value of `SendSimpleResp` or consider making it void if it really doesn't matter.

@abh3 and @ccaffy may have different opinions, so let's hear from them as well.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-25T14:54:15Z
user: login: bbockelm
body: Mostly stylistic & small fixes requested.

A few open-ended questions, particularly about how to phase things in, I'd like @amadio to chime in on.

I would like to do the improvements over a small series (3-5) of PRs instead of coming in with one giant overhaul.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T15:09:07Z
user: login: amadio
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T17:34:47Z
user: login: amadio
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-25T17:35:48Z
user: login: amadio
body: After looking more thoroughly, maybe parts of this are better for 5.9 (like adding the new `WebdavErrorMessage` function). However, the other changes would be ok for 5.8.3.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T15:01:11Z
user: login: dynamic-entropy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T18:33:30Z
user: login: dynamic-entropy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T18:34:44Z
user: login: dynamic-entropy
body: Made the requested changes. Thanks for taking a look.
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-20T20:26:17Z
user: login: dynamic-entropy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-28T08:17:12Z
user: login: dynamic-entropy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-28T08:17:55Z

Review comments:
diff_hunk: @@ -0,0 +1,79 @@
+#include <string>
path: src/XrdHttp/XrdHttpError.hh
user: login: bbockelm
body: Please correct header ordering.

I think we put package-specific headers first, then a blank line, then C++ standard headers.  Each block should be alphabetically ordered.

(@amadio did I get this correct?)
created_at: 2025-04-25T14:39:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,79 @@
+#include <string>
+#include <sstream>
+#include "XProtocol/XProtocol.hh"
+#include "XrdHttpReq.hh"
+
+
+class XrdHttpError {
path: src/XrdHttp/XrdHttpError.hh
user: login: bbockelm
body: Please add documentation string explaining the class and how it's supposed to work.
created_at: 2025-04-25T14:40:14Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,79 @@
+#include <string>
+#include <sstream>
+#include "XProtocol/XProtocol.hh"
+#include "XrdHttpReq.hh"
+
+
+class XrdHttpError {
+    private:
+        // HTTP response parameters to be sent back to the user
+        int httpStatusCode;
+        std::string httpErrorCode;
+        std::string httpStatusText;
+        const char *desc;
+        const char *header_to_add;
path: src/XrdHttp/XrdHttpError.hh
user: login: bbockelm
body: Please add doc strings for each member, especially useful for those using VSCode for inline information.
created_at: 2025-04-25T14:40:36Z
author_association: COLLABORATOR
diff_hunk: @@ -2369,7 +2370,11 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
 
 
         // If we are here it's too late to send a proper error message...
-        if (xrdresp == kXR_error) return -1;
+        if (xrdresp == kXR_error) {
+          XrdHttpError errResp = XrdHttpError::Create(xrdresp, xrderrcode, XrdHttpReq::rtPUT, (XRequestTypes)ntohs(xrdreq.header.requestid), etext, NULL, NULL);
path: src/XrdHttp/XrdHttpReq.cc
user: login: amadio
body: What is the advantage of using `XrdHttpError::Create` over simply having a constructor with equivalent arguments?
```suggestion
          XrdHttpError errResp(xrdresp, xrderrcode, XrdHttpReq::rtPUT, (XRequestTypes)ntohs(xrdreq.header.requestid), etext, NULL, NULL);
```
created_at: 2025-04-25T14:41:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,79 @@
+#include <string>
+#include <sstream>
+#include "XProtocol/XProtocol.hh"
+#include "XrdHttpReq.hh"
+
+
+class XrdHttpError {
+    private:
+        // HTTP response parameters to be sent back to the user
+        int httpStatusCode;
+        std::string httpErrorCode;
+        std::string httpStatusText;
+        const char *desc;
+        const char *header_to_add;
+
+        static std::string formatStatusText(std::string errCode, std::string etext) {
+        return ("ERROR: " + errCode + ": " + etext);
path: src/XrdHttp/XrdHttpError.hh
user: login: bbockelm
body: - Please fix indention issues and remove unnecessary `(`.
- Use a const reference for the input parameters (`const std::string &`) instead of a copy.
- Add doc string to the function (and throughout the class - won't explicitly mention this elsewhere).
created_at: 2025-04-25T14:42:21Z
author_association: COLLABORATOR
diff_hunk: @@ -2369,7 +2370,11 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
 
 
         // If we are here it's too late to send a proper error message...
-        if (xrdresp == kXR_error) return -1;
+        if (xrdresp == kXR_error) {
+          XrdHttpError errResp = XrdHttpError::Create(xrdresp, xrderrcode, XrdHttpReq::rtPUT, (XRequestTypes)ntohs(xrdreq.header.requestid), etext, NULL, NULL);
+          errResp.Send(prot, keepalive);
path: src/XrdHttp/XrdHttpReq.cc
user: login: amadio
body: Unused return value. It should probably be checked (I know it's not checked before, but if the return value doesn't matter, we can make it `void` instead).
created_at: 2025-04-25T14:42:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,79 @@
+#include <string>
+#include <sstream>
+#include "XProtocol/XProtocol.hh"
+#include "XrdHttpReq.hh"
+
+
+class XrdHttpError {
path: src/XrdHttp/XrdHttpError.hh
user: login: amadio
body: I think a class is a bit overkill for this, where maybe two or three free functions can do the same job better.
created_at: 2025-04-25T14:43:43Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,79 @@
+#include <string>
+#include <sstream>
+#include "XProtocol/XProtocol.hh"
+#include "XrdHttpReq.hh"
+
+
+class XrdHttpError {
+    private:
+        // HTTP response parameters to be sent back to the user
+        int httpStatusCode;
+        std::string httpErrorCode;
+        std::string httpStatusText;
+        const char *desc;
+        const char *header_to_add;
+
+        static std::string formatStatusText(std::string errCode, std::string etext) {
+        return ("ERROR: " + errCode + ": " + etext);
+        }
+
+    
+    public:
+        XrdHttpError(int code, const std::string &errCode, const std::string &statusText, const char *desc, const char *header_to_add)
path: src/XrdHttp/XrdHttpError.hh
user: login: bbockelm
body: I don't think we need both a factory function (`Create`) and a public constructor.  I don't have strong feelings as to which is better but it's confusing to have both.  @amadio, do you have stylistic opinions here?

If we stick with `Create`, let's make the constructor private.  While we're at it, we should probably delete the copy constructor.
created_at: 2025-04-25T14:44:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,79 @@
+#include <string>
+#include <sstream>
+#include "XProtocol/XProtocol.hh"
+#include "XrdHttpReq.hh"
+
+
+class XrdHttpError {
+    private:
+        // HTTP response parameters to be sent back to the user
+        int httpStatusCode;
+        std::string httpErrorCode;
+        std::string httpStatusText;
+        const char *desc;
+        const char *header_to_add;
+
+        static std::string formatStatusText(std::string errCode, std::string etext) {
+        return ("ERROR: " + errCode + ": " + etext);
+        }
+
+    
+    public:
+        XrdHttpError(int code, const std::string &errCode, const std::string &statusText, const char *desc, const char *header_to_add)
+        : httpStatusCode(code), httpErrorCode(errCode), httpStatusText(statusText), desc(desc), header_to_add(header_to_add) {}
+
+
+        static XrdHttpError Create( XResponseType xrdresp, XErrorCode xrderrcode, XrdHttpReq::ReqType httpVerb, XRequestTypes xrdOperation, std::string etext, const char *desc, const char *header_to_add){
+            int code{0};
+            std::string errCode;
+            std::string statusText;
+
+            switch (httpVerb){
path: src/XrdHttp/XrdHttpError.hh
user: login: amadio
body: I wonder if rather than using a `switch` we could have a large table with with all error types and just index into it to get the values.
created_at: 2025-04-25T14:46:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,79 @@
+#include <string>
+#include <sstream>
+#include "XProtocol/XProtocol.hh"
+#include "XrdHttpReq.hh"
+
+
+class XrdHttpError {
+    private:
+        // HTTP response parameters to be sent back to the user
+        int httpStatusCode;
+        std::string httpErrorCode;
+        std::string httpStatusText;
+        const char *desc;
+        const char *header_to_add;
+
+        static std::string formatStatusText(std::string errCode, std::string etext) {
+        return ("ERROR: " + errCode + ": " + etext);
+        }
+
+    
+    public:
+        XrdHttpError(int code, const std::string &errCode, const std::string &statusText, const char *desc, const char *header_to_add)
+        : httpStatusCode(code), httpErrorCode(errCode), httpStatusText(statusText), desc(desc), header_to_add(header_to_add) {}
+
+
+        static XrdHttpError Create( XResponseType xrdresp, XErrorCode xrderrcode, XrdHttpReq::ReqType httpVerb, XRequestTypes xrdOperation, std::string etext, const char *desc, const char *header_to_add){
path: src/XrdHttp/XrdHttpError.hh
user: login: bbockelm
body: - Small whitespace inconsistencies
- Make sure string inputs are `const std::string &`.
created_at: 2025-04-25T14:46:26Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,79 @@
+#include <string>
+#include <sstream>
+#include "XProtocol/XProtocol.hh"
+#include "XrdHttpReq.hh"
+
+
+class XrdHttpError {
+    private:
+        // HTTP response parameters to be sent back to the user
+        int httpStatusCode;
+        std::string httpErrorCode;
+        std::string httpStatusText;
+        const char *desc;
+        const char *header_to_add;
+
+        static std::string formatStatusText(std::string errCode, std::string etext) {
+        return ("ERROR: " + errCode + ": " + etext);
+        }
+
+    
+    public:
+        XrdHttpError(int code, const std::string &errCode, const std::string &statusText, const char *desc, const char *header_to_add)
+        : httpStatusCode(code), httpErrorCode(errCode), httpStatusText(statusText), desc(desc), header_to_add(header_to_add) {}
+
+
+        static XrdHttpError Create( XResponseType xrdresp, XErrorCode xrderrcode, XrdHttpReq::ReqType httpVerb, XRequestTypes xrdOperation, std::string etext, const char *desc, const char *header_to_add){
+            int code{0};
+            std::string errCode;
+            std::string statusText;
+
+            switch (httpVerb){
+                case XrdHttpReq::rtPUT: 
+                    if (xrdOperation == kXR_open){
+                        if (xrderrcode == kXR_isDirectory) {
+                            code = 409;
+                            errCode = "8.1";
+                        }
+                        else if (xrderrcode == kXR_NoSpace) {
+                            code = 507;
+                            errCode = "8.3.1";
+                        }
+                        else if (xrderrcode == kXR_overQuota){
+                            code = 507;
+                            errCode = "8.3.2";
+                        }
+                        else if (xrderrcode == kXR_NotAuthorized){
+                            code = 403;
+                            errCode = "9.3";
+                        }
+                    }
+                    else if (xrdOperation == kXR_write){
+                        if (xrderrcode == kXR_NoSpace) {
+                            code = 507;
+                            errCode = "8.4.1";
+                        }
+                        else if (xrderrcode == kXR_overQuota){
+                            code = 507;
+                            errCode = "8.4.2";
+                        }
+                    }
+                    else {
+                        code = 500;
+                        errCode = "Unknown";
+                    }
+                    break;
+                default:
+                    code = 500;
+                    errCode = "Internal Server Error";
+                    break;
path: src/XrdHttp/XrdHttpError.hh
user: login: amadio
body: This is confusing. Why have two distinct cases for 500? Can we let all 500 use `"Internal Server Error"`?
created_at: 2025-04-25T14:47:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,79 @@
+#include <string>
+#include <sstream>
+#include "XProtocol/XProtocol.hh"
+#include "XrdHttpReq.hh"
+
+
+class XrdHttpError {
+    private:
+        // HTTP response parameters to be sent back to the user
+        int httpStatusCode;
+        std::string httpErrorCode;
+        std::string httpStatusText;
+        const char *desc;
+        const char *header_to_add;
+
+        static std::string formatStatusText(std::string errCode, std::string etext) {
+        return ("ERROR: " + errCode + ": " + etext);
+        }
+
+    
+    public:
+        XrdHttpError(int code, const std::string &errCode, const std::string &statusText, const char *desc, const char *header_to_add)
+        : httpStatusCode(code), httpErrorCode(errCode), httpStatusText(statusText), desc(desc), header_to_add(header_to_add) {}
+
+
+        static XrdHttpError Create( XResponseType xrdresp, XErrorCode xrderrcode, XrdHttpReq::ReqType httpVerb, XRequestTypes xrdOperation, std::string etext, const char *desc, const char *header_to_add){
path: src/XrdHttp/XrdHttpError.hh
user: login: bbockelm
body: Please add a link to the wiki document in the doc string so those looking at the code know where some of the magic constants below (e.g., `8.3.1`) came from.
created_at: 2025-04-25T14:48:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,79 @@
+#include <string>
+#include <sstream>
+#include "XProtocol/XProtocol.hh"
+#include "XrdHttpReq.hh"
+
+
+class XrdHttpError {
+    private:
+        // HTTP response parameters to be sent back to the user
+        int httpStatusCode;
+        std::string httpErrorCode;
+        std::string httpStatusText;
+        const char *desc;
+        const char *header_to_add;
+
+        static std::string formatStatusText(std::string errCode, std::string etext) {
+        return ("ERROR: " + errCode + ": " + etext);
+        }
+
+    
+    public:
+        XrdHttpError(int code, const std::string &errCode, const std::string &statusText, const char *desc, const char *header_to_add)
+        : httpStatusCode(code), httpErrorCode(errCode), httpStatusText(statusText), desc(desc), header_to_add(header_to_add) {}
+
+
+        static XrdHttpError Create( XResponseType xrdresp, XErrorCode xrderrcode, XrdHttpReq::ReqType httpVerb, XRequestTypes xrdOperation, std::string etext, const char *desc, const char *header_to_add){
+            int code{0};
+            std::string errCode;
+            std::string statusText;
+
+            switch (httpVerb){
+                case XrdHttpReq::rtPUT: 
+                    if (xrdOperation == kXR_open){
+                        if (xrderrcode == kXR_isDirectory) {
+                            code = 409;
+                            errCode = "8.1";
+                        }
+                        else if (xrderrcode == kXR_NoSpace) {
+                            code = 507;
+                            errCode = "8.3.1";
+                        }
+                        else if (xrderrcode == kXR_overQuota){
+                            code = 507;
+                            errCode = "8.3.2";
+                        }
+                        else if (xrderrcode == kXR_NotAuthorized){
+                            code = 403;
+                            errCode = "9.3";
+                        }
+                    }
+                    else if (xrdOperation == kXR_write){
+                        if (xrderrcode == kXR_NoSpace) {
+                            code = 507;
+                            errCode = "8.4.1";
+                        }
+                        else if (xrderrcode == kXR_overQuota){
+                            code = 507;
+                            errCode = "8.4.2";
+                        }
+                    }
+                    else {
+                        code = 500;
+                        errCode = "Unknown";
+                    }
+                    break;
+                default:
+                    code = 500;
+                    errCode = "Internal Server Error";
+                    break;
+            }
+            statusText = formatStatusText(errCode, etext);
+            return XrdHttpError(code, errCode, statusText, desc, header_to_add);
+        }
+
+        int Send(XrdHttpProtocol *prot, bool keepalive){
+            return prot->SendSimpleResp(httpStatusCode, desc, header_to_add,
+                httpStatusText.c_str(), httpStatusText.length(), keepalive);
+        };
+};
path: src/XrdHttp/XrdHttpError.hh
user: login: bbockelm
body: Stylistically, please add a trailing newline.
created_at: 2025-04-25T14:48:37Z
author_association: COLLABORATOR
diff_hunk: @@ -2348,8 +2349,8 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
 
         if (xrdresp != kXR_ok) {
 
-          prot->SendSimpleResp(httpStatusCode, NULL, NULL,
-                               httpStatusText.c_str(), httpStatusText.length(), keepalive);
+          XrdHttpError errResp = XrdHttpError::Create(xrdresp, xrderrcode, XrdHttpReq::rtPUT, kXR_open, etext, NULL, NULL);
path: src/XrdHttp/XrdHttpReq.cc
user: login: bbockelm
body: Style comment: please use `auto` here.
created_at: 2025-04-25T14:49:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,79 @@
+#include <string>
+#include <sstream>
+#include "XProtocol/XProtocol.hh"
+#include "XrdHttpReq.hh"
+
+
+class XrdHttpError {
+    private:
+        // HTTP response parameters to be sent back to the user
+        int httpStatusCode;
+        std::string httpErrorCode;
+        std::string httpStatusText;
+        const char *desc;
+        const char *header_to_add;
+
+        static std::string formatStatusText(std::string errCode, std::string etext) {
+        return ("ERROR: " + errCode + ": " + etext);
+        }
+
+    
+    public:
+        XrdHttpError(int code, const std::string &errCode, const std::string &statusText, const char *desc, const char *header_to_add)
+        : httpStatusCode(code), httpErrorCode(errCode), httpStatusText(statusText), desc(desc), header_to_add(header_to_add) {}
+
+
+        static XrdHttpError Create( XResponseType xrdresp, XErrorCode xrderrcode, XrdHttpReq::ReqType httpVerb, XRequestTypes xrdOperation, std::string etext, const char *desc, const char *header_to_add){
+            int code{0};
+            std::string errCode;
+            std::string statusText;
+
+            switch (httpVerb){
+                case XrdHttpReq::rtPUT: 
+                    if (xrdOperation == kXR_open){
+                        if (xrderrcode == kXR_isDirectory) {
+                            code = 409;
+                            errCode = "8.1";
+                        }
+                        else if (xrderrcode == kXR_NoSpace) {
+                            code = 507;
+                            errCode = "8.3.1";
+                        }
+                        else if (xrderrcode == kXR_overQuota){
+                            code = 507;
+                            errCode = "8.3.2";
+                        }
+                        else if (xrderrcode == kXR_NotAuthorized){
+                            code = 403;
+                            errCode = "9.3";
+                        }
+                    }
+                    else if (xrdOperation == kXR_write){
+                        if (xrderrcode == kXR_NoSpace) {
+                            code = 507;
+                            errCode = "8.4.1";
+                        }
+                        else if (xrderrcode == kXR_overQuota){
+                            code = 507;
+                            errCode = "8.4.2";
+                        }
+                    }
+                    else {
+                        code = 500;
+                        errCode = "Unknown";
+                    }
+                    break;
+                default:
+                    code = 500;
+                    errCode = "Internal Server Error";
+                    break;
+            }
+            statusText = formatStatusText(errCode, etext);
+            return XrdHttpError(code, errCode, statusText, desc, header_to_add);
+        }
+
+        int Send(XrdHttpProtocol *prot, bool keepalive){
+            return prot->SendSimpleResp(httpStatusCode, desc, header_to_add,
+                httpStatusText.c_str(), httpStatusText.length(), keepalive);
path: src/XrdHttp/XrdHttpError.hh
user: login: amadio
body: The usage of this class seems to be always to create the error, call this, then destroy the object. I think the idea of just using a function for the combined operation taking all parameters and returning in one go might be cleaner.
created_at: 2025-04-25T14:49:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,79 @@
+#include <string>
+#include <sstream>
+#include "XProtocol/XProtocol.hh"
+#include "XrdHttpReq.hh"
+
+
+class XrdHttpError {
+    private:
+        // HTTP response parameters to be sent back to the user
+        int httpStatusCode;
+        std::string httpErrorCode;
+        std::string httpStatusText;
+        const char *desc;
+        const char *header_to_add;
+
+        static std::string formatStatusText(std::string errCode, std::string etext) {
+        return ("ERROR: " + errCode + ": " + etext);
+        }
+
+    
+    public:
+        XrdHttpError(int code, const std::string &errCode, const std::string &statusText, const char *desc, const char *header_to_add)
+        : httpStatusCode(code), httpErrorCode(errCode), httpStatusText(statusText), desc(desc), header_to_add(header_to_add) {}
+
+
+        static XrdHttpError Create( XResponseType xrdresp, XErrorCode xrderrcode, XrdHttpReq::ReqType httpVerb, XRequestTypes xrdOperation, std::string etext, const char *desc, const char *header_to_add){
+            int code{0};
+            std::string errCode;
+            std::string statusText;
+
+            switch (httpVerb){
+                case XrdHttpReq::rtPUT: 
+                    if (xrdOperation == kXR_open){
+                        if (xrderrcode == kXR_isDirectory) {
+                            code = 409;
+                            errCode = "8.1";
+                        }
+                        else if (xrderrcode == kXR_NoSpace) {
+                            code = 507;
+                            errCode = "8.3.1";
+                        }
+                        else if (xrderrcode == kXR_overQuota){
+                            code = 507;
+                            errCode = "8.3.2";
+                        }
+                        else if (xrderrcode == kXR_NotAuthorized){
+                            code = 403;
+                            errCode = "9.3";
+                        }
+                    }
+                    else if (xrdOperation == kXR_write){
+                        if (xrderrcode == kXR_NoSpace) {
+                            code = 507;
+                            errCode = "8.4.1";
+                        }
+                        else if (xrderrcode == kXR_overQuota){
+                            code = 507;
+                            errCode = "8.4.2";
+                        }
+                    }
+                    else {
+                        code = 500;
+                        errCode = "Unknown";
path: src/XrdHttp/XrdHttpError.hh
user: login: bbockelm
body: Here and below (the `default` case), we unconditionally overwrite any status code and error text set in the existing [`XrdHttpReq::mapXrdErrorToHttpStatus`](https://github.com/xrootd/xrootd/blob/master/src/XrdHttp/XrdHttpReq.cc#L875).

That means if we don't handle every single case covered by `XrdHttpReq::mapXrdErrorToHttpStatus`, then we will regress on the error messages for those cases.

Since we want a gradual transition - and not to do the entirety of the project in a single PR - let's have a way we can fallback to the errors generated by `XrdHttpReq::mapXrdErrorToHttpStatus` and, when the project is complete, we can add back this `else` and remove the other function.
created_at: 2025-04-25T14:55:29Z
author_association: COLLABORATOR
diff_hunk: @@ -2369,7 +2370,11 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
 
 
         // If we are here it's too late to send a proper error message...
-        if (xrdresp == kXR_error) return -1;
path: src/XrdHttp/XrdHttpReq.cc
user: login: bbockelm
body: We can't do this, unfortunately.

Per the comment above, we have already started the response and HTTP rules are that you can't send a second response to the same request.  This change breaks the HTTP protocol framing.

For `GET`, we have a small extension where the client can opt-in to a trailer (which is part of the HTTP protocol though rarely used) that indicates whether an error occurred while sending the `GET` response.  We could do a similar thing for `PUT` -- however, for now, we should keep the behavior of just abruptly closing the socket and save the refactor of the `PUT` response for later.
created_at: 2025-04-25T15:00:51Z
author_association: COLLABORATOR
diff_hunk: @@ -189,4 +189,62 @@ function test_http() {
   receivedHeader=$(grep -i 'Test:' "$outputFilePath")
   assert_eq "1" "$(echo "$receivedHeader" | wc -l | sed 's/^ *//')" "Incorrect number of 'Test' header values"
   assert_eq "$expectedHeader" "$receivedHeader" "HEAD is missing statically-defined Test header"
+
+  set -ex
path: tests/XRootD/http.sh
user: login: bbockelm
body: Let's avoid setting `e` here at least; we should explicitly check for failures where needed (`e` can have bad interactions with traps).

`x` I'm indifferent on but likely should be cleaned up to have the unit test be less verbose.
created_at: 2025-04-25T15:04:00Z
author_association: COLLABORATOR
diff_hunk: @@ -189,4 +189,62 @@ function test_http() {
   receivedHeader=$(grep -i 'Test:' "$outputFilePath")
   assert_eq "1" "$(echo "$receivedHeader" | wc -l | sed 's/^ *//')" "Incorrect number of 'Test' header values"
   assert_eq "$expectedHeader" "$receivedHeader" "HEAD is missing statically-defined Test header"
+
+  set -ex
+
+  run_and_assert_http_and_error_code() {
+    local expected_http_code="$1"
+    local expected_error_code="$2"
+    shift 2
+
+    local body_file
+    body_file=$(mktemp)
+    local http_code
+
+    # Run the curl command, capture HTTP code and body
+    http_code=$(curl -s -L -w "%{http_code}" -o "$body_file" "$@")
+    local body
+    body=$(< "$body_file")
+    rm -f "$body_file"
+
+    # Print body to stderr for debug visibility
+    echo "$body" >&2
path: tests/XRootD/http.sh
user: login: bbockelm
body: Delay printing out the body until an error has occurred.  That'll keep the tests less chatty for successes.
created_at: 2025-04-25T15:05:33Z
author_association: COLLABORATOR
diff_hunk: @@ -28,6 +28,7 @@ if(NOT ENABLE_SERVER_TESTS)
   return()
 endif()
 
+add_subdirectory( XrdOssTests )
path: tests/CMakeLists.txt
user: login: bbockelm
body: @amadio - do we have to worry about race conditions between the build and the running of the tests?

Here, `XRootD::http::test` will require the built library.  I assume that `ctest` is guaranteed to only run after a full build so we don't need any explicit dependency in CMake.

Is that assumption correct?
created_at: 2025-04-25T15:07:58Z
author_association: COLLABORATOR
diff_hunk: @@ -189,4 +189,62 @@ function test_http() {
   receivedHeader=$(grep -i 'Test:' "$outputFilePath")
   assert_eq "1" "$(echo "$receivedHeader" | wc -l | sed 's/^ *//')" "Incorrect number of 'Test' header values"
   assert_eq "$expectedHeader" "$receivedHeader" "HEAD is missing statically-defined Test header"
+
+  set -ex
path: tests/XRootD/http.sh
user: login: amadio
body: I agree, let's try not to make tests too verbose. A clear message on failure is more than enough. Tests that pass don't need to create any output.
created_at: 2025-04-25T17:34:46Z
author_association: MEMBER
diff_hunk: @@ -28,6 +28,7 @@ if(NOT ENABLE_SERVER_TESTS)
   return()
 endif()
 
+add_subdirectory( XrdOssTests )
path: tests/CMakeLists.txt
user: login: amadio
body: Yes, `ctest` runs only after everything is already built.
created_at: 2025-04-25T17:35:48Z
author_association: MEMBER
diff_hunk: @@ -1631,19 +1643,17 @@ int XrdHttpProtocol::StartSimpleResp(int code, const char *desc, const char *hea
   } else {
     ss << m_staticheaders[""];
   }
-  
+
   if ((bodylen >= 0) && (code != 100))
     ss << "Content-Length: " << bodylen << crlf;
 
-  if (header_to_add && (header_to_add[0] != '\0'))
-    ss << header_to_add << crlf;
+  if (header_to_add && (header_to_add[0] != '\0')) ss << header_to_add << crlf;
 
   ss << crlf;
 
   const std::string &outhdr = ss.str();
   TRACEI(RSP, "Sending resp: " << code << " header len:" << outhdr.size());
-  if (SendData(outhdr.c_str(), outhdr.size()))
-    return -1;
+  if (SendData(outhdr.c_str(), outhdr.size())) return -1;
path: src/XrdHttp/XrdHttpProtocol.cc
user: login: amadio
body: This change and the one above are just formatting, please remove (it's better to have the different line numbers for different statements for debugging).
created_at: 2025-05-20T14:48:59Z
author_association: MEMBER
diff_hunk: @@ -818,38 +818,89 @@ void XrdHttpReq::parseResource(char *res) {
   
 }
 
+void XrdHttpReq::sendWebdavErrorMessage(
+    XResponseType xrdresp, XErrorCode xrderrcode, XrdHttpReq::ReqType httpVerb,
+    XRequestTypes xrdOperation, std::string etext, const char *desc,
+    const char *header_to_add, bool keepalive) {
+  int code{0};
+  std::string errCode{"Unknown"};
+  std::string statusText;
+
+  switch (httpVerb) {
+    case XrdHttpReq::rtPUT:
path: src/XrdHttp/XrdHttpReq.cc
user: login: amadio
body: This is a switch with a single case. I think it might be better to start with a switch for the inner part and use an if for the httpVerb, then move to a switch later if necessary. Not a requirement to change, though.
created_at: 2025-05-20T14:51:38Z
author_association: MEMBER
diff_hunk: @@ -861,46 +861,46 @@ void XrdHttpReq::sendWebdavErrorMessage(
   if (code != 0) {
     httpStatusCode = code;
     httpErrorCode = errCode;
-    httpStatusText = "ERROR: " + errCode + ": " + etext + "\n";
-
-    prot->SendSimpleResp(httpStatusCode, desc, header_to_add,
-                         httpStatusText.c_str(), httpStatusText.length(),
-                         keepalive);
+    httpErrorBody = "ERROR: " + errCode + ": " + etext + "\n";
   }
+
+  prot->SendSimpleResp(httpStatusCode, desc, header_to_add,
+                       httpErrorBody.c_str(), httpErrorBody.length(),
+                       keepalive);
path: src/XrdHttp/XrdHttpReq.cc
user: login: amadio
body: This contradicts the commit message. Why is `prot->SendSimpleResp` moved out of the `if (code != 0)` block?
created_at: 2025-05-20T14:55:21Z
author_association: MEMBER
diff_hunk: @@ -1631,19 +1643,17 @@ int XrdHttpProtocol::StartSimpleResp(int code, const char *desc, const char *hea
   } else {
     ss << m_staticheaders[""];
   }
-  
+
   if ((bodylen >= 0) && (code != 100))
     ss << "Content-Length: " << bodylen << crlf;
 
-  if (header_to_add && (header_to_add[0] != '\0'))
-    ss << header_to_add << crlf;
+  if (header_to_add && (header_to_add[0] != '\0')) ss << header_to_add << crlf;
 
   ss << crlf;
 
   const std::string &outhdr = ss.str();
   TRACEI(RSP, "Sending resp: " << code << " header len:" << outhdr.size());
-  if (SendData(outhdr.c_str(), outhdr.size()))
-    return -1;
+  if (SendData(outhdr.c_str(), outhdr.size())) return -1;
path: src/XrdHttp/XrdHttpProtocol.cc
user: login: dynamic-entropy
body: Sure!
created_at: 2025-05-20T18:33:30Z
author_association: COLLABORATOR
diff_hunk: @@ -861,46 +861,46 @@ void XrdHttpReq::sendWebdavErrorMessage(
   if (code != 0) {
     httpStatusCode = code;
     httpErrorCode = errCode;
-    httpStatusText = "ERROR: " + errCode + ": " + etext + "\n";
-
-    prot->SendSimpleResp(httpStatusCode, desc, header_to_add,
-                         httpStatusText.c_str(), httpStatusText.length(),
-                         keepalive);
+    httpErrorBody = "ERROR: " + errCode + ": " + etext + "\n";
   }
+
+  prot->SendSimpleResp(httpStatusCode, desc, header_to_add,
+                       httpErrorBody.c_str(), httpErrorBody.length(),
+                       keepalive);
path: src/XrdHttp/XrdHttpReq.cc
user: login: dynamic-entropy
body: Seems like an oversight when rebasing. Will fix it. Thanks for catching it.
created_at: 2025-05-20T18:34:44Z
author_association: COLLABORATOR
diff_hunk: @@ -818,38 +818,89 @@ void XrdHttpReq::parseResource(char *res) {
   
 }
 
+void XrdHttpReq::sendWebdavErrorMessage(
+    XResponseType xrdresp, XErrorCode xrderrcode, XrdHttpReq::ReqType httpVerb,
+    XRequestTypes xrdOperation, std::string etext, const char *desc,
+    const char *header_to_add, bool keepalive) {
+  int code{0};
+  std::string errCode{"Unknown"};
+  std::string statusText;
+
+  switch (httpVerb) {
+    case XrdHttpReq::rtPUT:
path: src/XrdHttp/XrdHttpReq.cc
user: login: dynamic-entropy
body: I anticipate adding other verbs as cases, so using a switch now helps avoid refactoring later — either way shouldn’t make much difference for now.
created_at: 2025-05-20T18:42:51Z
author_association: COLLABORATOR
diff_hunk: @@ -2369,7 +2370,11 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {
 
 
         // If we are here it's too late to send a proper error message...
-        if (xrdresp == kXR_error) return -1;
+        if (xrdresp == kXR_error) {
+          XrdHttpError errResp = XrdHttpError::Create(xrdresp, xrderrcode, XrdHttpReq::rtPUT, (XRequestTypes)ntohs(xrdreq.header.requestid), etext, NULL, NULL);
path: src/XrdHttp/XrdHttpReq.cc
user: login: dynamic-entropy
body: Changed back from the use of the factory pattern.
created_at: 2025-05-28T08:17:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,79 @@
+#include <string>
+#include <sstream>
+#include "XProtocol/XProtocol.hh"
+#include "XrdHttpReq.hh"
+
+
+class XrdHttpError {
+    private:
+        // HTTP response parameters to be sent back to the user
+        int httpStatusCode;
+        std::string httpErrorCode;
+        std::string httpStatusText;
+        const char *desc;
+        const char *header_to_add;
+
+        static std::string formatStatusText(std::string errCode, std::string etext) {
+        return ("ERROR: " + errCode + ": " + etext);
+        }
+
+    
+    public:
+        XrdHttpError(int code, const std::string &errCode, const std::string &statusText, const char *desc, const char *header_to_add)
+        : httpStatusCode(code), httpErrorCode(errCode), httpStatusText(statusText), desc(desc), header_to_add(header_to_add) {}
+
+
+        static XrdHttpError Create( XResponseType xrdresp, XErrorCode xrderrcode, XrdHttpReq::ReqType httpVerb, XRequestTypes xrdOperation, std::string etext, const char *desc, const char *header_to_add){
+            int code{0};
+            std::string errCode;
+            std::string statusText;
+
+            switch (httpVerb){
path: src/XrdHttp/XrdHttpError.hh
user: login: dynamic-entropy
body: To be resolved in the upcoming changes.
created_at: 2025-05-28T08:17:55Z
author_association: COLLABORATOR

PR comments:
user: login: abh3
created_at: 2025-05-03T05:47:24Z
body: I must admit this comment got buried on all of the traffic. First, I am a firm believer that the simplest solution is usually the best one. In this particular case, I can only fine a quibble of whether to use a table or a map. If the number of error codes is small and will not substantially grow, then a table is fine. Otherwise, I would be in favor of a map. I would also be in favor of a map if the growth of error codes is ill-defined and may actually substantially increase given the current trends. I also would add that if a map leads to a simpler implementation, then I would be in favor of a map. I think the problem here is that map and table initialization or not exactly the same. One occurs at compile time and the other at run time, though it's a trivial difference especially since it occurs only once. All things being equal, I'd go with the map.

Anyway, those are my thoughts.

Andy
________________________________
From: Guilherme Amadio ***@***.***>
Sent: Friday, April 25, 2025 7:54 AM
To: xrootd/xrootd ***@***.***>
Cc: Andrew Hanushevsky ***@***.***>; Mention ***@***.***>
Subject: Re: [xrootd/xrootd] Add webdav error codes for file path and permission errors (PR #2496)


BEWARE: This email originated outside of our organization. DO NOT CLICK links or attachments unless you recognize the sender and know the content is safe.


@amadio requested changes on this pull request.

This is a good start, but I think that a class is not necessary, we can have a table with all the errors in it, a function to index into this table to get the actual error properties, and another function to format the string and call prot->SendSimpleResp(...). We should also check the return value of SendSimpleResp or consider making it void if it really doesn't matter.

@abh3<https://github.com/abh3> and @ccaffy<https://github.com/ccaffy> may have different opinions, so let's hear from them as well.

________________________________

In src/XrdHttp/XrdHttpReq.cc<https://github.com/xrootd/xrootd/pull/2496#discussion_r2060378023>:

> @@ -2369,7 +2370,11 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {


         // If we are here it's too late to send a proper error message...
-        if (xrdresp == kXR_error) return -1;
+        if (xrdresp == kXR_error) {
+          XrdHttpError errResp = XrdHttpError::Create(xrdresp, xrderrcode, XrdHttpReq::rtPUT, (XRequestTypes)ntohs(xrdreq.header.requestid), etext, NULL, NULL);


What is the advantage of using XrdHttpError::Create over simply having a constructor with equivalent arguments?

⬇️ Suggested change

-          XrdHttpError errResp = XrdHttpError::Create(xrdresp, xrderrcode, XrdHttpReq::rtPUT, (XRequestTypes)ntohs(xrdreq.header.requestid), etext, NULL, NULL);
+          XrdHttpError errResp(xrdresp, xrderrcode, XrdHttpReq::rtPUT, (XRequestTypes)ntohs(xrdreq.header.requestid), etext, NULL, NULL);


________________________________

In src/XrdHttp/XrdHttpReq.cc<https://github.com/xrootd/xrootd/pull/2496#discussion_r2060380457>:

> @@ -2369,7 +2370,11 @@ int XrdHttpReq::PostProcessHTTPReq(bool final_) {


         // If we are here it's too late to send a proper error message...
-        if (xrdresp == kXR_error) return -1;
+        if (xrdresp == kXR_error) {
+          XrdHttpError errResp = XrdHttpError::Create(xrdresp, xrderrcode, XrdHttpReq::rtPUT, (XRequestTypes)ntohs(xrdreq.header.requestid), etext, NULL, NULL);
+          errResp.Send(prot, keepalive);


Unused return value. It should probably be checked (I know it's not checked before, but if the return value doesn't matter, we can make it void instead).

________________________________

In src/XrdHttp/XrdHttpError.hh<https://github.com/xrootd/xrootd/pull/2496#discussion_r2060381873>:

> @@ -0,0 +1,79 @@
+#include <string>
+#include <sstream>
+#include "XProtocol/XProtocol.hh"
+#include "XrdHttpReq.hh"
+
+
+class XrdHttpError {


I think a class is a bit overkill for this, where maybe two or three free functions can do the same job better.

________________________________

In src/XrdHttp/XrdHttpError.hh<https://github.com/xrootd/xrootd/pull/2496#discussion_r2060385795>:

> +        static std::string formatStatusText(std::string errCode, std::string etext) {
+        return ("ERROR: " + errCode + ": " + etext);
+        }
+
+
+    public:
+        XrdHttpError(int code, const std::string &errCode, const std::string &statusText, const char *desc, const char *header_to_add)
+        : httpStatusCode(code), httpErrorCode(errCode), httpStatusText(statusText), desc(desc), header_to_add(header_to_add) {}
+
+
+        static XrdHttpError Create( XResponseType xrdresp, XErrorCode xrderrcode, XrdHttpReq::ReqType httpVerb, XRequestTypes xrdOperation, std::string etext, const char *desc, const char *header_to_add){
+            int code{0};
+            std::string errCode;
+            std::string statusText;
+
+            switch (httpVerb){


I wonder if rather than using a switch we could have a large table with with all error types and just index into it to get the values.

________________________________

In src/XrdHttp/XrdHttpError.hh<https://github.com/xrootd/xrootd/pull/2496#discussion_r2060389433>:

> +                    else {
+                        code = 500;
+                        errCode = "Unknown";
+                    }
+                    break;
+                default:
+                    code = 500;
+                    errCode = "Internal Server Error";
+                    break;


This is confusing. Why have two distinct cases for 500? Can we let all 500 use "Internal Server Error"?

________________________________

In src/XrdHttp/XrdHttpError.hh<https://github.com/xrootd/xrootd/pull/2496#discussion_r2060393042>:

> +        int Send(XrdHttpProtocol *prot, bool keepalive){
+            return prot->SendSimpleResp(httpStatusCode, desc, header_to_add,
+                httpStatusText.c_str(), httpStatusText.length(), keepalive);


The usage of this class seems to be always to create the error, call this, then destroy the object. I think the idea of just using a function for the combined operation taking all parameters and returning in one go might be cleaner.

—
Reply to this email directly, view it on GitHub<https://github.com/xrootd/xrootd/pull/2496#pullrequestreview-2794370672>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AAUIW57G2UW6YJG722HPBZL23JEC3AVCNFSM6AAAAAB33QRPUCVHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHMZDOOJUGM3TANRXGI>.
You are receiving this because you were mentioned.Message ID: ***@***.***>

author_association: MEMBER
user: login: amadio
created_at: 2025-05-20T12:33:58Z
body: @dynamic-entropy Could you please rebase this on top of the current master branch? I will prepare a 5.8.3 release soon (waiting for some fixes that need to go into EOS), and I'd like to include the changes from this pull request. Thank you.
author_association: MEMBER