diff --git a/ddsc/core/download.py b/ddsc/core/download.py
index 545d438..b8ec446 100644
--- a/ddsc/core/download.py
+++ b/ddsc/core/download.py
@@ -12,6 +12,8 @@ from ddsc.core.remotestore import RemoteStore, ProjectFile, RemoteFileUrl
 FETCH_EXTERNAL_PUT_RETRY_TIMES = 5
 FETCH_EXTERNAL_RETRY_SECONDS = 20
 RESOURCE_NOT_CONSISTENT_RETRY_SECONDS = 2
+SWIFT_EXPIRED_STATUS_CODE = 401
+S3_EXPIRED_STATUS_CODE = 403
 
 
 class ProjectDownload(object):
@@ -421,7 +423,10 @@ class RetryChunkDownloader(object):
         headers = self.get_range_headers()
         if file_download.http_headers:
             headers.update(file_download.http_headers)
-        url = '{}/{}'.format(file_download.host, file_download.url)
+        separator = ""
+        if not file_download.url.startswith("/"):
+            separator = "/"
+        url = '{}{}{}'.format(file_download.host, separator, file_download.url)
         return url, headers
 
     def get_range_headers(self):
@@ -436,7 +441,8 @@ class RetryChunkDownloader(object):
         :param headers: dict: headers used to download this file chunk
         """
         response = requests.get(url, headers=headers, stream=True)
-        if response.status_code == 401:
+        if response.status_code == SWIFT_EXPIRED_STATUS_CODE \
+                or response.status_code == S3_EXPIRED_STATUS_CODE:
             raise DownloadInconsistentError(response.text)
         response.raise_for_status()
         self.actual_bytes_read = 0

Error 403 downloading project from s3 backend
Error printed out by ddsclient:
```
requests.exceptions.HTTPError: 403 Client Error: Forbidden for 
url: https://eso.duhs.duke.edu//ca....
```
Payload returned by S3:
```
<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated
does not match the signature you provided. Check your Secret Access Key and signing method. 
For more information, see REST Authentication and SOAP Authentication for details.
</Message><RequestId>...</RequestId></Error
```

Error seems to be due to an extra slash in the url.

Removing the slash from the following line fixes this issue:
https://github.com/Duke-GCB/DukeDSClient/blob/95e2d44613240a68d78da8c887acfbcab18f5136/ddsc/core/download.py#L424


diff --git a/ddsc/core/tests/test_download.py b/ddsc/core/tests/test_download.py
index 7535a22..00a620c 100644
--- a/ddsc/core/tests/test_download.py
+++ b/ddsc/core/tests/test_download.py
@@ -438,9 +438,23 @@ class TestRetryChunkDownloader(TestCase):
         with self.assertRaises(DownloadInconsistentError):
             downloader.retry_download_loop()
 
-    def test_get_url_and_headers_for_range(self):
+    def test_get_url_and_headers_for_range_with_no_slashes(self):
         mock_context = Mock()
         mock_file_download = Mock(host='somehost', url='someurl')
+        mock_file_download.http_headers = {}
+        downloader = RetryChunkDownloader(project_file=None, local_path=None, seek_amt=None,
+                                          bytes_to_read=None, download_context=mock_context)
+
+        downloader.get_range_headers = Mock()
+        downloader.get_range_headers.return_value = {'Range': 'bytes=100-200'}
+
+        url, headers = downloader.get_url_and_headers_for_range(mock_file_download)
+
+        self.assertEqual(url, 'somehost/someurl')
+
+    def test_get_url_and_headers_for_range(self):
+        mock_context = Mock()
+        mock_file_download = Mock(host='somehost', url='/someurl')
         mock_file_download.http_headers = {'secret': '222'}
         downloader = RetryChunkDownloader(project_file=None, local_path=None, seek_amt=None,
                                           bytes_to_read=None, download_context=mock_context)
@@ -463,16 +477,28 @@ class TestRetryChunkDownloader(TestCase):
         self.assertEqual(downloader.get_range_headers(), {'Range': 'bytes=200-299'})
 
     @patch('ddsc.core.download.requests')
-    def test_download_chunk_inconsistent(self, mock_requests):
+    def test_download_chunk_inconsistent_swift(self, mock_requests):
+        mock_context = Mock()
+        downloader = RetryChunkDownloader(project_file=None, local_path=None, seek_amt=None,
+                                          bytes_to_read=None, download_context=mock_context)
+
+        mock_requests.get.return_value = Mock(status_code=401, text='Bad Swift Download')
+
+        with self.assertRaises(DownloadInconsistentError) as raised_exception:
+            downloader.download_chunk('someurl', {})
+            self.assertEqual(str(raised_exception), 'Bad Swift Download')
+
+    @patch('ddsc.core.download.requests')
+    def test_download_chunk_inconsistent_s3(self, mock_requests):
         mock_context = Mock()
         downloader = RetryChunkDownloader(project_file=None, local_path=None, seek_amt=None,
                                           bytes_to_read=None, download_context=mock_context)
 
-        mock_requests.get.return_value = Mock(status_code=401, text='Bad Download')
+        mock_requests.get.return_value = Mock(status_code=403, text='Bad S3 Download')
 
         with self.assertRaises(DownloadInconsistentError) as raised_exception:
             downloader.download_chunk('someurl', {})
-            self.assertEqual(str(raised_exception), 'Bad Download')
+            self.assertEqual(str(raised_exception), 'Bad S3 Download')
 
     @patch('ddsc.core.download.requests')
     def test_download_chunk_works(self, mock_requests):

pip install -e .[dev]
parse_log_pytest
requirements.txt
mock flake8 nose pytest
3.7
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
-e git+https://github.com/Duke-GCB/DukeDSClient.git@95e2d44613240a68d78da8c887acfbcab18f5136#egg=DukeDSClient
exceptiongroup==1.2.2
flake8==5.0.4
future==0.16.0
idna==3.10
importlib-metadata==4.2.0
iniconfig==2.0.0
mccabe==0.7.0
mock==5.2.0
nose==1.3.7
packaging==24.0
pluggy==1.2.0
pycodestyle==2.9.1
pyflakes==2.5.0
pytest==7.4.4
pytz==2025.2
PyYAML==3.12
requests==2.31.0
six==1.10.0
tomli==2.0.1
typing_extensions==4.7.1
urllib3==2.0.7
zipp==3.15.0

name: DukeDSClient
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - charset-normalizer==3.4.1
      - exceptiongroup==1.2.2
      - flake8==5.0.4
      - future==0.16.0
      - idna==3.10
      - importlib-metadata==4.2.0
      - iniconfig==2.0.0
      - mccabe==0.7.0
      - mock==5.2.0
      - nose==1.3.7
      - packaging==24.0
      - pluggy==1.2.0
      - pycodestyle==2.9.1
      - pyflakes==2.5.0
      - pytest==7.4.4
      - pytz==2025.2
      - pyyaml==3.12
      - requests==2.31.0
      - six==1.10.0
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - zipp==3.15.0
prefix: /opt/conda/envs/DukeDSClient

ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_download_chunk_inconsistent_s3 ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_get_url_and_headers_for_range
ddsc/core/tests/test_download.py::TestProjectDownload::test_check_warnings ddsc/core/tests/test_download.py::TestProjectDownload::test_include_project_file ddsc/core/tests/test_download.py::TestProjectDownload::test_run ddsc/core/tests/test_download.py::TestProjectDownload::test_run_preprocessor ddsc/core/tests/test_download.py::TestDownloadSettings::test_get_data_service_auth_data ddsc/core/tests/test_download.py::TestDownloadContext::test_create_data_service ddsc/core/tests/test_download.py::TestDownloadContext::test_create_remote_store ddsc/core/tests/test_download.py::TestDownloadContext::test_send_error_message ddsc/core/tests/test_download.py::TestDownloadContext::test_send_message ddsc/core/tests/test_download.py::TestDownloadContext::test_send_processed_message ddsc/core/tests/test_download.py::TestFileUrlDownloader::test_check_downloaded_files_sizes ddsc/core/tests/test_download.py::TestFileUrlDownloader::test_check_file_size ddsc/core/tests/test_download.py::TestFileUrlDownloader::test_determine_bytes_per_chunk ddsc/core/tests/test_download.py::TestFileUrlDownloader::test_download_files ddsc/core/tests/test_download.py::TestFileUrlDownloader::test_make_big_empty_files ddsc/core/tests/test_download.py::TestFileUrlDownloader::test_make_local_directories ddsc/core/tests/test_download.py::TestFileUrlDownloader::test_make_ranges ddsc/core/tests/test_download.py::TestFileUrlDownloader::test_split_file_urls_by_size ddsc/core/tests/test_download.py::TestDownloadFilePartCommand::test_create_context ddsc/core/tests/test_download.py::TestDownloadFilePartCommand::test_on_message_error ddsc/core/tests/test_download.py::TestDownloadFilePartCommand::test_on_message_processed ddsc/core/tests/test_download.py::TestDownloadFilePartRun::test_download_file_part_run ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_download_chunk_inconsistent_swift ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_download_chunk_works ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_get_range_headers ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_get_url_and_headers_for_range_with_no_slashes ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_on_bytes_read ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_on_bytes_read_too_much ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_retry_download_loop ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_retry_download_loop_raises_when_out_of_retries ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_retry_download_loop_retries_then_works ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_retry_download_loop_too_few_actual_bytes_read ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_retry_download_loop_too_few_actual_bytes_read_then_works ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_run_handles_exception ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_verify_download_complete_ok ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_verify_download_complete_partial ddsc/core/tests/test_download.py::TestRetryChunkDownloader::test_verify_download_complete_too_large
ddsc/core/download.py
ddsc/core/download.py