As I ran into this as well, and likely many more will, this is my solution:
If you look at torch.utils.data.dataloader._BaseDataLoaderIter 7` you will find:
def _next_index(self):
    return next(self._sampler_iter)  # may raise StopIteration

so a StopIteration is raised if your Sampler does so. You will also find in the DataLoader class itself:
    def __len__(self):
        if self._dataset_kind == _DatasetKind.Iterable:
            # (...) cut the comment here
            length = self._IterableDataset_len_called = len(self.dataset)
            return length
        else:
            return len(self._index_sampler)

that the length is defined by the _index_sampler if you do not have an IterableDataset. I think that the most elegant solution here is to have an infinite data sampler with no length (as the length is not required in the dataloader, see the comments in the code.
Something like this would be a good sampler:
class InfiniteSampler:
    def __init__(self, dataset_size):
        self.dataset_size = dataset_size

    def __iter__(self):
        yield from itertools.islice(self._infinite(), 0, None, 1) # Infinite iterator

    def _infinite(self):
        g = torch.Generator()
        while True:
            yield from torch.randperm(self.dataset_size, generator=g)

A similar approach is currently taken in Detectron2 56.