Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle 412 response #203

Closed
armpogart opened this issue May 10, 2020 · 4 comments · Fixed by #245
Closed

Handle 412 response #203

armpogart opened this issue May 10, 2020 · 4 comments · Fixed by #245

Comments

@armpogart
Copy link

While using putFileContents('path', buffer, { overwrite: false }) I get following error: Request failed with status code 412.

P.S. Without { overwrite: false } everything works, but obviously it just overwrites the files, but I need to not overwrite files if they exist. Desktop clients (such as WinSCP and Cyberduck) successfully detect existing files and don't overwrite them if requested. I do not have control over DAV server.

The full error object is the following (sensitive information stripped):

{
    config: {
      url: 'https://<striped>/dav/product_images/test/VGUNRC108-230-BLK-0.jpg',
      method: 'put',
      data: <Buffer ff d8 ff e1 00 5c 45 78 69 66 00 00 49 49 2a 00 08 00 00 00 01 00 98 82 02 00 37 00 00 00 1a 00 00 00 00 00 00 00 20 20 20 20 20 20 20 20 20 20 20 20 ... 112686 more bytes>,
      headers: {
        Accept: 'application/json, text/plain, */*',
        'Content-Type': 'application/octet-stream',
        'Content-Length': 112736,
        'If-None-Match': '*',
        Authorization: 'Digest username="<striped>", realm="SabreDAV", nonce="<striped>", uri="/dav/product_images/test/VGUNRC108-230-BLK-0.jpg", qop=auth, response="<striped>", nc=00
    000001, cnonce="<striped>", algorithm=md5, opaque="<striped>"',
        'User-Agent': 'axios/0.19.2'
      },
      transformRequest: [ [Function: transformRequest] ],
      transformResponse: [ [Function: transformResponse] ],
      timeout: 0,
      adapter: [Function: httpAdapter],
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxContentLength: -1,
      validateStatus: [Function]
    },
    request: ClientRequest {
      _events: [Object: null prototype] {
        socket: [Function],
        abort: [Function],
        aborted: [Function],
        error: [Function],
        timeout: [Function],
        prefinish: [Function: requestOnPrefinish]
      },
      _eventsCount: 6,
      _maxListeners: undefined,
      outputData: [],
      outputSize: 0,
      writable: true,
      _last: true,
      chunkedEncoding: false,
      shouldKeepAlive: false,
      useChunkedEncodingByDefault: true,
      sendDate: false,
      _removedConnection: false,
      _removedContLen: false,
      _removedTE: false,
      _contentLength: null,
      _hasBody: true,
      _trailer: '',
      finished: true,
      _headerSent: true,
      socket: TLSSocket {
        _tlsOptions: [Object],
        _secureEstablished: true,
        _securePending: false,
        _newSessionPending: false,
        _controlReleased: true,
        _SNICallback: null,
        servername: '<striped>',
        alpnProtocol: false,
        authorized: true,
        authorizationError: null,
        encrypted: true,
        _events: [Object: null prototype],
        _eventsCount: 10,
        connecting: false,
        _hadError: false,
        _parent: null,
        _host: '<striped>',
        _readableState: [ReadableState],
        readable: true,
        _maxListeners: undefined,
        _writableState: [WritableState],
        writable: false,
        allowHalfOpen: false,
        _sockname: null,
        _pendingData: null,
        _pendingEncoding: '',
        server: undefined,
        _server: null,
        ssl: [TLSWrap],
        _requestCert: true,
        _rejectUnauthorized: true,
        parser: null,
        _httpMessage: [Circular],
        [Symbol(res)]: [TLSWrap],
        [Symbol(asyncId)]: 9437,
        [Symbol(kHandle)]: [TLSWrap],
        [Symbol(kSetNoDelay)]: false,
        [Symbol(lastWriteQueueSize)]: 0,
        [Symbol(timeout)]: null,
        [Symbol(kBuffer)]: null,
        [Symbol(kBufferCb)]: null,
        [Symbol(kBufferGen)]: null,
        [Symbol(kCapture)]: false,
        [Symbol(kBytesRead)]: 0,
        [Symbol(kBytesWritten)]: 0,
        [Symbol(connect-options)]: [Object]
      },
      connection: TLSSocket {
        _tlsOptions: [Object],
        _secureEstablished: true,
        _securePending: false,
        _newSessionPending: false,
        _controlReleased: true,
        _SNICallback: null,
        servername: '<striped>',
        alpnProtocol: false,
        authorized: true,
        authorizationError: null,
        encrypted: true,
        _events: [Object: null prototype],
        _eventsCount: 10,
        connecting: false,
        _hadError: false,
        _parent: null,
        _host: '<striped>',
        _readableState: [ReadableState],
        readable: true,
        _maxListeners: undefined,
        _writableState: [WritableState],
        writable: false,
        allowHalfOpen: false,
        _sockname: null,
        _pendingData: null,
        _pendingEncoding: '',
        server: undefined,
        _server: null,
        ssl: [TLSWrap],
        _requestCert: true,
        _rejectUnauthorized: true,
        parser: null,
        _httpMessage: [Circular],
        [Symbol(res)]: [TLSWrap],
        [Symbol(asyncId)]: 9437,
        [Symbol(kHandle)]: [TLSWrap],
        [Symbol(kSetNoDelay)]: false,
        [Symbol(lastWriteQueueSize)]: 0,
        [Symbol(timeout)]: null,
        [Symbol(kBuffer)]: null,
        [Symbol(kBufferCb)]: null,
        [Symbol(kBufferGen)]: null,
        [Symbol(kCapture)]: false,
        [Symbol(kBytesRead)]: 0,
        [Symbol(kBytesWritten)]: 0,
        [Symbol(connect-options)]: [Object]
      },
      _header: 'PUT /dav/product_images/test/VGUNRC108-230-BLK-0.jpg HTTP/1.1\r\n' +
        'Accept: application/json, text/plain, */*\r\n' +
        'Content-Type: application/octet-stream\r\n' +
        'Content-Length: 112736\r\n' +
        'If-None-Match: *\r\n' +
        'Authorization: Digest username="<striped>", realm="SabreDAV", nonce="<striped>", uri="/dav/product_images/test/VGUNRC108-230-BLK-0.jpg", qop=auth, response="<striped>", nc=00
    000001, cnonce="<striped>", algorithm=md5, opaque="<striped>"\r\n' +
        'User-Agent: axios/0.19.2\r\n' +
        'Host: <striped>\r\n' +
        'Connection: close\r\n' +
        '\r\n',
      _onPendingData: [Function: noopPendingOutput],
      agent: Agent {
        _events: [Object: null prototype],
        _eventsCount: 2,
        _maxListeners: undefined,
        defaultPort: 443,
        protocol: 'https:',
        options: [Object],
        requests: {},
        sockets: [Object],
        freeSockets: {},
        keepAliveMsecs: 1000,
        keepAlive: false,
        maxSockets: Infinity,
        maxFreeSockets: 256,
        maxCachedSessions: 100,
        _sessionCache: [Object],
        [Symbol(kCapture)]: false
      },
      socketPath: undefined,
      method: 'PUT',
      insecureHTTPParser: undefined,
      path: '/dav/product_images/test/VGUNRC108-230-BLK-0.jpg',
      _ended: true,
      res: IncomingMessage {
        _readableState: [ReadableState],
        readable: false,
        _events: [Object: null prototype],
        _eventsCount: 3,
        _maxListeners: undefined,
        socket: [TLSSocket],
        connection: [TLSSocket],
        httpVersionMajor: 1,
        httpVersionMinor: 1,
        httpVersion: '1.1',
        complete: true,
        headers: [Object],
        rawHeaders: [Array],
        trailers: {},
        rawTrailers: [],
        aborted: false,
        upgrade: false,
        url: '',
        method: null,
        statusCode: 412,
        statusMessage: 'Precondition failed',
        client: [TLSSocket],
        _consuming: false,
        _dumped: false,
        req: [Circular],
        responseUrl: 'https://<striped>/dav/product_images/test/VGUNRC108-230-BLK-0.jpg',
        redirects: [],
        [Symbol(kCapture)]: false
      },
      aborted: false,
      timeoutCb: null,
      upgradeOrConnect: false,
      parser: null,
      maxHeadersCount: null,
      reusedSocket: false,
      _redirectable: Writable {
        _writableState: [WritableState],
        writable: true,
        _events: [Object: null prototype],
        _eventsCount: 2,
        _maxListeners: undefined,
        _options: [Object],
        _redirectCount: 0,
        _redirects: [],
        _requestBodyLength: 112736,
        _requestBodyBuffers: [],
        _onNativeResponse: [Function],
        _currentRequest: [Circular],
        _currentUrl: 'https://<striped>/dav/product_images/test/VGUNRC108-230-BLK-0.jpg',
        [Symbol(kCapture)]: false
      },
      [Symbol(kCapture)]: false,
      [Symbol(kNeedDrain)]: false,
      [Symbol(corked)]: 0,
      [Symbol(kOutHeaders)]: [Object: null prototype] {
        accept: [Array],
        'content-type': [Array],
        'content-length': [Array],
        'if-none-match': [Array],
        authorization: [Array],
        'user-agent': [Array],
        host: [Array]
      }
    },
    response: {
      status: 412,
      statusText: 'Precondition failed',
      headers: {
        server: 'openresty',
        date: 'Sun, 10 May 2020 15:48:50 GMT',
        'content-type': 'application/xml; charset=utf-8',
        'transfer-encoding': 'chunked',
        connection: 'close',
        'x-request-id': 'cc6bed0a43fe3b2fe8dc51b32f1aaf4b'
      },
      config: {
        url: 'https://<striped>/dav/product_images/test/VGUNRC108-230-BLK-0.jpg',
        method: 'put',
        data: <Buffer ff d8 ff e1 00 5c 45 78 69 66 00 00 49 49 2a 00 08 00 00 00 01 00 98 82 02 00 37 00 00 00 1a 00 00 00 00 00 00 00 20 20 20 20 20 20 20 20 20 20 20 20 ... 112686 more bytes>,
        headers: [Object],
        transformRequest: [Array],
        transformResponse: [Array],
        timeout: 0,
        adapter: [Function: httpAdapter],
        xsrfCookieName: 'XSRF-TOKEN',
        xsrfHeaderName: 'X-XSRF-TOKEN',
        maxContentLength: -1,
        validateStatus: [Function]
      },
      request: ClientRequest {
        _events: [Object: null prototype],
        _eventsCount: 6,
        _maxListeners: undefined,
        outputData: [],
        outputSize: 0,
        writable: true,
        _last: true,
        chunkedEncoding: false,
        shouldKeepAlive: false,
        useChunkedEncodingByDefault: true,
        sendDate: false,
        _removedConnection: false,
        _removedContLen: false,
        _removedTE: false,
        _contentLength: null,
        _hasBody: true,
        _trailer: '',
        finished: true,
        _headerSent: true,
        socket: [TLSSocket],
        connection: [TLSSocket],
        _header: 'PUT /dav/product_images/test/VGUNRC108-230-BLK-0.jpg HTTP/1.1\r\n' +
          'Accept: application/json, text/plain, */*\r\n' +
          'Content-Type: application/octet-stream\r\n' +
          'Content-Length: 112736\r\n' +
          'If-None-Match: *\r\n' +
          'Authorization: Digest username="<striped>", realm="SabreDAV", nonce="<striped>", uri="/dav/product_images/test/VGUNRC108-230-BLK-0.jpg", qop=auth, response="<striped>", nc=
    00000001, cnonce="<striped>", algorithm=md5, opaque="<striped>"\r\n' +
          'User-Agent: axios/0.19.2\r\n' +
          'Host: <striped>\r\n' +
          'Connection: close\r\n' +
          '\r\n',
        _onPendingData: [Function: noopPendingOutput],
        agent: [Agent],
        socketPath: undefined,
        method: 'PUT',
        insecureHTTPParser: undefined,
        path: '/dav/product_images/test/VGUNRC108-230-BLK-0.jpg',
        _ended: true,
        res: [IncomingMessage],
        aborted: false,
        timeoutCb: null,
        upgradeOrConnect: false,
        parser: null,
        maxHeadersCount: null,
        reusedSocket: false,
        _redirectable: [Writable],
        [Symbol(kCapture)]: false,
        [Symbol(kNeedDrain)]: false,
        [Symbol(corked)]: 0,
        [Symbol(kOutHeaders)]: [Object: null prototype]
      },
      data: '<?xml version="1.0" encoding="utf-8"?>\n' +
        '<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">\n' +
        '  <s:exception>Sabre\\DAV\\Exception\\PreconditionFailed</s:exception>\n' +
        '  <s:message>An If-None-Match header was specified, but the ETag matched (or * was specified).</s:message>\n' +
        '  <s:header>If-None-Match</s:header>\n' +
        '</d:error>\n'
    },
    isAxiosError: true,
    toJSON: [Function]
}
@perry-mitchell
Copy link
Owner

@armpogart Any info as to what your server is? Nextcloud or something? Thanks.

@armpogart
Copy link
Author

@perry-mitchell Not sure, I'm using the library on some SAAS service which provides dav access for storage. The only thing I see is that they are using SabreDAV. The workaround I currently use for the issue is exists before putFileContents which is obviously making me to make 2 requests (which slows down the workflow) instead of one.

@andrey-pavlenko
Copy link

I encountered a similar error in the function putFileContents on box.com and yandex.ru.

await client.putFileContents(path, message + '\n', { overwrite: false });

If file not exists -- Ok, if file exists -- error:

// box.com
{
  config: {
    url: 'https://dav.box.com/dav/test/test.txt',
    method: 'put',
    data: 'String 2\n',
    headers: {
      Accept: 'application/json, text/plain, */*',
      'Content-Type': 'application/octet-stream',
      'Content-Length': 9,
      Authorization: 'Basic XXX=',
      'If-None-Match': '*',
      'User-Agent': 'axios/0.19.2'
    },
    timeout: 0,
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
  },
  // ...
  response: {
    status: 412,
    statusText: 'Precondition Failed',
    headers: {
      date: 'Mon, 08 Jun 2020 07:34:28 GMT',
      'content-type': 'application/xml',
      'content-length': '320',
      connection: 'close',
      'x-envoy-upstream-service-time': '133',
      'strict-transport-security': 'max-age=31536000'
    },
    config: {
      url: 'https://dav.box.com/dav/test/test.txt',
      method: 'put',
      data: 'String 2\n',
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
    },
    data: '<?xml version="1.0" encoding="utf-8"?>\n' +
      '<d:error xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns">\n' +
      '  <s:exception>Sabre_DAV_Exception_PreconditionFailed</s:exception>\n' +
      '  <s:message>An If-None-Match header was specified, but the ETag matched (or * was specified).</s:message>\n' +
      '  <s:header>If-None-Match</s:header>\n' +
      '</d:error>\n'
  },
  isAxiosError: true,
}
// yandex.ru
{
  config: {
    url: 'https://webdav.yandex.ru/test/test.txt',
    method: 'put',
    data: 'String 2\n',
    headers: {
      Accept: 'application/json, text/plain, */*',
      'Content-Type': 'application/octet-stream',
      'Content-Length': 9,
      Authorization: 'Basic XXX==',
      'If-None-Match': '*',
      'User-Agent': 'axios/0.19.2'
    },
    timeout: 0,
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
  },
  // ...
  response: {
    status: 412,
    statusText: 'Precondition Failed',
    headers: {
      connection: 'close',
      date: 'Mon, 08 Jun 2020 10:26:20 GMT',
      'yandex-cloud-request-id': 'dav-XXX',
      'yandex-uid': 'XXX',
      'x-frame-options': 'SAMEORIGIN',
      'x-xss-protection': '1; mode=block',
      'x-content-type-options': 'nosniff',
      server: 'Jetty(9.4.11.v20180605)'
    },
    config: {
      url: 'https://webdav.yandex.ru/test/test.txt',
      method: 'put',
      data: 'String 2\n',
      timeout: 0,
      xsrfCookieName: 'XSRF-TOKEN',
      xsrfHeaderName: 'X-XSRF-TOKEN',
      maxContentLength: -1,
    },
    data: ''
  },
  isAxiosError: true,
}

@armpogart
Copy link
Author

@perry-mitchell After further digging into the problem I've found out that server is responding correctly and the problem is in the client (this library). From RFC7232 on Conditional Requests you can see following excerpt:

An origin server that receives an If-None-Match header field MUST
evaluate the condition prior to performing the method (Section 5).
If the field-value is "*", the condition is false if the origin
server has a current representation for the target resource. If the
field-value is a list of entity-tags, the condition is false if one
of the listed tags match the entity-tag of the selected
representation.

An origin server MUST NOT perform the requested method if the
condition evaluates to false; instead, the origin server MUST respond
with either a) the 304 (Not Modified) status code if the request
method is GET or HEAD or b) the 412 (Precondition Failed) status code
for all other request methods.

and so the server responds with 412 which means that it has a current representation for the target resource (in our case file that we want to upload). But in this implementation we can see the code that throws error if the response error code >= 400.

I'm not sure how or on which server this method was tested with overwrite, and if the test succeeded it simply meant that the server wasn't correctly implementing HTTP RFC.

@perry-mitchell Would you accept PR on this one? Is it sufficient to just return the response with that status code in that case and exclude it from >=400 check?

P.S. If there is no any precondition check with ETag specified (and as far as I see there is none) my proposed change will be ok. If ETag is used anywhere 412 Precondition Failed can also mean that the file was changed on the server side between first upload request (after which client cached ETag from response) and now. In that case I guess returning that response is still ok, as we don't want overwrite the file, even if it is not the same file that we have uploaded before (as we are currently comparing only the file name). And also as a remark, I think 412 response is not invalid response anyways, so we don't need to throw error and can just return that and leave the user ability to decide what to do (we can just document it somewhere).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants